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 CompetitionHome from './components/CompetitionHome'
|
||||
import StandingsTable from './components/StandingsTable'
|
||||
import TeamMatchesModal from './components/TeamMatchesModal'
|
||||
import CompetitionPicker from './components/CompetitionPicker'
|
||||
import COMPETITIONS from './competitions'
|
||||
import { apiFetch } from './api'
|
||||
import styles from './App.module.css'
|
||||
|
||||
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 [competition, setCompetition] = useState(null)
|
||||
const [season, setSeason] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [selectedTeam, setSelectedTeam] = useState(null)
|
||||
// Cache competition emblems keyed by code so the picker can show them
|
||||
const [emblems, setEmblems] = useState({})
|
||||
|
||||
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(() => {
|
||||
apiFetch('/competitions')
|
||||
.then((data) => {
|
||||
@@ -28,10 +29,12 @@ export default function App() {
|
||||
}
|
||||
setEmblems(map)
|
||||
})
|
||||
.catch(() => {}) // non-critical — picker still works without logos
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Fetch standings when a competition is selected
|
||||
useEffect(() => {
|
||||
if (!selectedCode) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setStandings(null)
|
||||
@@ -51,29 +54,46 @@ export default function App() {
|
||||
})
|
||||
}, [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 (
|
||||
<div className={styles.app}>
|
||||
<header className={styles.header}>
|
||||
{competition?.emblem && (
|
||||
<img src={competition.emblem} alt={competition.name} className={styles.emblem} />
|
||||
<button className={styles.backBtn} onClick={handleBack} aria-label="Back">
|
||||
←
|
||||
</button>
|
||||
{(competition?.emblem || emblems[selectedCode]) && (
|
||||
<img
|
||||
src={competition?.emblem ?? emblems[selectedCode]}
|
||||
alt={competition?.name ?? compMeta?.name}
|
||||
className={styles.emblem}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h1 className={styles.title}>{competition?.name ?? '—'}</h1>
|
||||
<h1 className={styles.title}>{competition?.name ?? compMeta?.name ?? '—'}</h1>
|
||||
{season && (
|
||||
<p className={styles.season}>
|
||||
{formatSeason(season)}
|
||||
{season.currentMatchday != null && ` — Speelronde ${season.currentMatchday}`}
|
||||
{season.currentMatchday != null && ` — Matchday ${season.currentMatchday}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<CompetitionPicker
|
||||
selected={selectedCode}
|
||||
onChange={setSelectedCode}
|
||||
emblems={emblems}
|
||||
/>
|
||||
|
||||
<main className={styles.main}>
|
||||
{loading && <Spinner />}
|
||||
{error && <ErrorMessage message={error} />}
|
||||
@@ -82,7 +102,7 @@ export default function App() {
|
||||
)}
|
||||
{!loading && !error && standings?.length === 0 && (
|
||||
<p style={{ color: '#666', textAlign: 'center', padding: '3rem' }}>
|
||||
Geen standen beschikbaar voor deze competitie.
|
||||
No standings available for this competition.
|
||||
</p>
|
||||
)}
|
||||
</main>
|
||||
@@ -120,7 +140,7 @@ function Spinner() {
|
||||
}}
|
||||
/>
|
||||
<style>{`@keyframes spin { to { transform: rotate(360deg) } }`}</style>
|
||||
<p>Laden…</p>
|
||||
<p>Loading…</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -138,7 +158,7 @@ function ErrorMessage({ message }) {
|
||||
margin: '2rem auto',
|
||||
}}
|
||||
>
|
||||
<strong>Fout:</strong> {message}
|
||||
<strong>Error:</strong> {message}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,24 @@
|
||||
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 {
|
||||
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