From b070b9111543cc2d2662bc91f69095f1c563a181 Mon Sep 17 00:00:00 2001 From: henry Date: Tue, 17 Mar 2026 20:14:45 +0100 Subject: [PATCH] Replace competition picker bar with full-page tile home screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/App.jsx | 58 ++++++---- src/App.module.css | 18 +++ src/components/CompetitionHome.jsx | 32 ++++++ src/components/CompetitionHome.module.css | 132 ++++++++++++++++++++++ 4 files changed, 221 insertions(+), 19 deletions(-) create mode 100644 src/components/CompetitionHome.jsx create mode 100644 src/components/CompetitionHome.module.css diff --git a/src/App.jsx b/src/App.jsx index f49604f..767d019 100644 --- a/src/App.jsx +++ b/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 + } + + const compMeta = COMPETITIONS.find((c) => c.code === selectedCode) + return (
- {competition?.emblem && ( - {competition.name} + + {(competition?.emblem || emblems[selectedCode]) && ( + {competition?.name )}
-

{competition?.name ?? '—'}

+

{competition?.name ?? compMeta?.name ?? '—'}

{season && (

{formatSeason(season)} - {season.currentMatchday != null && ` — Speelronde ${season.currentMatchday}`} + {season.currentMatchday != null && ` — Matchday ${season.currentMatchday}`}

)}
- -
{loading && } {error && } @@ -82,7 +102,7 @@ export default function App() { )} {!loading && !error && standings?.length === 0 && (

- Geen standen beschikbaar voor deze competitie. + No standings available for this competition.

)}
@@ -120,7 +140,7 @@ function Spinner() { }} /> -

Laden…

+

Loading…

) } @@ -138,7 +158,7 @@ function ErrorMessage({ message }) { margin: '2rem auto', }} > - Fout: {message} + Error: {message} ) } diff --git a/src/App.module.css b/src/App.module.css index 80e55e9..398f682 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -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%; } diff --git a/src/components/CompetitionHome.jsx b/src/components/CompetitionHome.jsx new file mode 100644 index 0000000..01f68b7 --- /dev/null +++ b/src/components/CompetitionHome.jsx @@ -0,0 +1,32 @@ +import COMPETITIONS from '../competitions' +import styles from './CompetitionHome.module.css' + +export default function CompetitionHome({ emblems, onSelect }) { + return ( +
+
+

Football Competitions

+

Select a competition to view the standings

+
+ +
+ {COMPETITIONS.map((comp) => ( + + ))} +
+
+ ) +} diff --git a/src/components/CompetitionHome.module.css b/src/components/CompetitionHome.module.css new file mode 100644 index 0000000..5bc1c15 --- /dev/null +++ b/src/components/CompetitionHome.module.css @@ -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; + } +}