Replace competition picker bar with full-page tile home screen
- New CompetitionHome page shows all competitions as clickable tiles in a responsive grid (4 → 3 → 2 columns) - Selecting a tile navigates to the standings page - Standings page has a back button to return to the home screen - Emblems pre-loaded on mount so tiles show logos immediately - Remove CompetitionPicker component from standings view Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
58
src/App.jsx
58
src/App.jsx
@@ -1,24 +1,25 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import CompetitionHome from './components/CompetitionHome'
|
||||||
import StandingsTable from './components/StandingsTable'
|
import StandingsTable from './components/StandingsTable'
|
||||||
import TeamMatchesModal from './components/TeamMatchesModal'
|
import TeamMatchesModal from './components/TeamMatchesModal'
|
||||||
import CompetitionPicker from './components/CompetitionPicker'
|
import COMPETITIONS from './competitions'
|
||||||
import { apiFetch } from './api'
|
import { apiFetch } from './api'
|
||||||
import styles from './App.module.css'
|
import styles from './App.module.css'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [selectedCode, setSelectedCode] = useState('DED')
|
const [page, setPage] = useState('home') // 'home' | 'standings'
|
||||||
|
const [selectedCode, setSelectedCode] = useState(null)
|
||||||
const [standings, setStandings] = useState(null)
|
const [standings, setStandings] = useState(null)
|
||||||
const [competition, setCompetition] = useState(null)
|
const [competition, setCompetition] = useState(null)
|
||||||
const [season, setSeason] = useState(null)
|
const [season, setSeason] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null)
|
const [selectedTeam, setSelectedTeam] = useState(null)
|
||||||
// Cache competition emblems keyed by code so the picker can show them
|
|
||||||
const [emblems, setEmblems] = useState({})
|
const [emblems, setEmblems] = useState({})
|
||||||
|
|
||||||
const closeModal = useCallback(() => setSelectedTeam(null), [])
|
const closeModal = useCallback(() => setSelectedTeam(null), [])
|
||||||
|
|
||||||
// Load all competition emblems once on mount so the picker shows logos immediately
|
// Load all competition emblems once on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiFetch('/competitions')
|
apiFetch('/competitions')
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -28,10 +29,12 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
setEmblems(map)
|
setEmblems(map)
|
||||||
})
|
})
|
||||||
.catch(() => {}) // non-critical — picker still works without logos
|
.catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Fetch standings when a competition is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!selectedCode) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setStandings(null)
|
setStandings(null)
|
||||||
@@ -51,29 +54,46 @@ export default function App() {
|
|||||||
})
|
})
|
||||||
}, [selectedCode])
|
}, [selectedCode])
|
||||||
|
|
||||||
|
function handleSelect(code) {
|
||||||
|
setSelectedCode(code)
|
||||||
|
setPage('standings')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
setPage('home')
|
||||||
|
setSelectedTeam(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page === 'home') {
|
||||||
|
return <CompetitionHome emblems={emblems} onSelect={handleSelect} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const compMeta = COMPETITIONS.find((c) => c.code === selectedCode)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<header className={styles.header}>
|
<header className={styles.header}>
|
||||||
{competition?.emblem && (
|
<button className={styles.backBtn} onClick={handleBack} aria-label="Back">
|
||||||
<img src={competition.emblem} alt={competition.name} className={styles.emblem} />
|
←
|
||||||
|
</button>
|
||||||
|
{(competition?.emblem || emblems[selectedCode]) && (
|
||||||
|
<img
|
||||||
|
src={competition?.emblem ?? emblems[selectedCode]}
|
||||||
|
alt={competition?.name ?? compMeta?.name}
|
||||||
|
className={styles.emblem}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className={styles.title}>{competition?.name ?? '—'}</h1>
|
<h1 className={styles.title}>{competition?.name ?? compMeta?.name ?? '—'}</h1>
|
||||||
{season && (
|
{season && (
|
||||||
<p className={styles.season}>
|
<p className={styles.season}>
|
||||||
{formatSeason(season)}
|
{formatSeason(season)}
|
||||||
{season.currentMatchday != null && ` — Speelronde ${season.currentMatchday}`}
|
{season.currentMatchday != null && ` — Matchday ${season.currentMatchday}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<CompetitionPicker
|
|
||||||
selected={selectedCode}
|
|
||||||
onChange={setSelectedCode}
|
|
||||||
emblems={emblems}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{loading && <Spinner />}
|
{loading && <Spinner />}
|
||||||
{error && <ErrorMessage message={error} />}
|
{error && <ErrorMessage message={error} />}
|
||||||
@@ -82,7 +102,7 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
{!loading && !error && standings?.length === 0 && (
|
{!loading && !error && standings?.length === 0 && (
|
||||||
<p style={{ color: '#666', textAlign: 'center', padding: '3rem' }}>
|
<p style={{ color: '#666', textAlign: 'center', padding: '3rem' }}>
|
||||||
Geen standen beschikbaar voor deze competitie.
|
No standings available for this competition.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
@@ -120,7 +140,7 @@ function Spinner() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||||
<p>Laden…</p>
|
<p>Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -138,7 +158,7 @@ function ErrorMessage({ message }) {
|
|||||||
margin: '2rem auto',
|
margin: '2rem auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>Fout:</strong> {message}
|
<strong>Error:</strong> {message}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,24 @@
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backBtn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.4rem 0.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn:hover {
|
||||||
|
border-color: #e87722;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/components/CompetitionHome.jsx
Normal file
32
src/components/CompetitionHome.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import COMPETITIONS from '../competitions'
|
||||||
|
import styles from './CompetitionHome.module.css'
|
||||||
|
|
||||||
|
export default function CompetitionHome({ emblems, onSelect }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<h1 className={styles.title}>Football Competitions</h1>
|
||||||
|
<p className={styles.subtitle}>Select a competition to view the standings</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.grid}>
|
||||||
|
{COMPETITIONS.map((comp) => (
|
||||||
|
<button
|
||||||
|
key={comp.code}
|
||||||
|
className={styles.tile}
|
||||||
|
onClick={() => onSelect(comp.code)}
|
||||||
|
>
|
||||||
|
<div className={styles.emblemWrap}>
|
||||||
|
{emblems[comp.code]
|
||||||
|
? <img src={emblems[comp.code]} alt={comp.name} className={styles.emblem} />
|
||||||
|
: <div className={styles.emblemPlaceholder}>{comp.code}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span className={styles.name}>{comp.name}</span>
|
||||||
|
<span className={styles.country}>{comp.country}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
132
src/components/CompetitionHome.module.css
Normal file
132
src/components/CompetitionHome.module.css
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 3rem 1.5rem 4rem;
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tile grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
background: #161616;
|
||||||
|
border: 1px solid #242424;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, border-color 0.15s, background 0.15s;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile:hover {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-color: #e87722;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emblemWrap {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emblem {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emblemPlaceholder {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.page {
|
||||||
|
padding: 2rem 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
padding: 1.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emblemWrap,
|
||||||
|
.emblem,
|
||||||
|
.emblemPlaceholder {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user