diff --git a/src/features/live/LivePage.tsx b/src/features/live/LivePage.tsx index f7f6452..acf6d43 100644 --- a/src/features/live/LivePage.tsx +++ b/src/features/live/LivePage.tsx @@ -3,6 +3,7 @@ import { Radio } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { MatchCard } from '@/components/MatchCard'; import { useTournamentStore } from '@/stores/tournamentStore'; +import { useFavoriteStore } from '@/stores/favoriteStore'; import { isToday } from '@/lib/format'; import { upcomingByDay, type DayGroup } from '@/lib/fixtures'; import { fmt, useFormat } from '@/lib/i18n'; @@ -24,8 +25,17 @@ function Section({ title, fixtures }: { title: string; fixtures: Fixture[] }) { export function LivePage() { const snapshot = useTournamentStore((s) => s.snapshot); + const favorite = useFavoriteStore((s) => s.favorite); const { t, kickoffDay } = useFormat(); + // The starred team's next (or running) match, pinned on top. + const favMatch = useMemo(() => { + if (!favorite || !snapshot) return null; + return snapshot.fixtures + .filter((f) => f.status !== 'finished' && (f.home.team === favorite || f.away.team === favorite)) + .sort((a, b) => a.kickoff.localeCompare(b.kickoff))[0] ?? null; + }, [favorite, snapshot]); + const groups = useMemo(() => { const fixtures = snapshot?.fixtures ?? []; const live = fixtures.filter((f) => f.status === 'live').sort((a, b) => a.kickoff.localeCompare(b.kickoff)); @@ -66,6 +76,15 @@ export function LivePage() { subtitle={fmt(t.live.subtitle, { season: snapshot.season })} /> + {favMatch && ( +
+

★ {t.live.myTeam}

+
+ +
+
+ )} +
{groups.upcoming.map((g, i) => ( diff --git a/src/features/team/TeamProfilePage.tsx b/src/features/team/TeamProfilePage.tsx index 9209bca..0610dbe 100644 --- a/src/features/team/TeamProfilePage.tsx +++ b/src/features/team/TeamProfilePage.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { Link, useParams } from '@tanstack/react-router'; -import { ArrowLeft, Shield } from 'lucide-react'; +import { ArrowLeft, Shield, Star } from 'lucide-react'; +import { useFavoriteStore } from '@/stores/favoriteStore'; import { Card, CardBody, CardHeader } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { teamFlag } from '@/lib/teams'; @@ -31,6 +32,24 @@ function FixtureRow({ f, team }: { f: Fixture; team: string }) { ); } +function FavoriteStar({ team }: { team: string }) { + const t = useT(); + const favorite = useFavoriteStore((s) => s.favorite); + const toggle = useFavoriteStore((s) => s.toggleFavorite); + const active = favorite === team; + return ( + + ); +} + export function TeamProfilePage() { const { name } = useParams({ strict: false }) as { name: string }; const t = useT(); @@ -62,7 +81,10 @@ export function TeamProfilePage() { {teamFlag(profile.team)}
-

{profile.team}

+

+ {profile.team} + +

{profile.group && {fmt(t.common.group, { group: profile.group })}} {fmt(t.team.eloBadge, { rating: profile.elo })} diff --git a/src/lib/i18n/de.ts b/src/lib/i18n/de.ts index dfab195..1dc2455 100644 --- a/src/lib/i18n/de.ts +++ b/src/lib/i18n/de.ts @@ -66,6 +66,7 @@ export const de: Dict = { nextUp: "Als Nächstes", nextUpDay: "Als Nächstes · {day}", comingUp: "Demnächst", + myTeam: "Mein Team", latestResults: "Letzte Ergebnisse", empty: "Der Spielplan ist geladen. Sobald das Turnier läuft, tauchen die Spiele hier auf.", }, @@ -371,6 +372,8 @@ export const de: Dict = { team: { unknown: "Unbekanntes Team.", allTeams: "Alle Teams", + follow: "Diesem Team folgen", + unfollow: "Diesem Team nicht mehr folgen", eloBadge: "Elo {rating}", titleOddsBadge: "Titel {pct}", advanceOddsBadge: "Weiterkommen {pct}", diff --git a/src/lib/i18n/en.ts b/src/lib/i18n/en.ts index 9c1625c..1900bc2 100644 --- a/src/lib/i18n/en.ts +++ b/src/lib/i18n/en.ts @@ -65,6 +65,7 @@ export const en = { nextUp: "Next up", nextUpDay: "Next up · {day}", comingUp: "Coming up", + myTeam: "My team", latestResults: "Latest results", empty: "The schedule is ready. Matches will appear here once the tournament kicks off.", }, @@ -370,6 +371,8 @@ export const en = { team: { unknown: "Unknown team.", allTeams: "All teams", + follow: "Follow this team", + unfollow: "Unfollow this team", eloBadge: "Elo {rating}", titleOddsBadge: "Title {pct}", advanceOddsBadge: "Advance {pct}", diff --git a/src/stores/favoriteStore.ts b/src/stores/favoriteStore.ts new file mode 100644 index 0000000..15764ed --- /dev/null +++ b/src/stores/favoriteStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; + +const STORAGE_KEY = 'cup26:favorite'; + +function readFavorite(): string | null { + try { + return localStorage.getItem(STORAGE_KEY); + } catch { + return null; + } +} + +interface FavoriteState { + favorite: string | null; + toggleFavorite: (team: string) => void; +} + +/** One starred team — its next match gets pinned on the Live tab. */ +export const useFavoriteStore = create((set, get) => ({ + favorite: readFavorite(), + toggleFavorite: (team) => { + const next = get().favorite === team ? null : team; + try { + if (next) localStorage.setItem(STORAGE_KEY, next); + else localStorage.removeItem(STORAGE_KEY); + } catch { /* private mode */ } + set({ favorite: next }); + }, +}));