Favorite team: star a side, get its next match pinned on Live
A star on the team page (localStorage, no accounts) pins that team's next or running match in a My team section at the top of the Live tab. EN/DE strings + aria-pressed on the toggle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 && (
|
||||
<section className="mb-7">
|
||||
<h2 className="smallcaps mb-2.5">★ {t.live.myTeam}</h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<MatchCard f={favMatch} />
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Section title={t.live.liveNowHeading} fixtures={groups.live} />
|
||||
<Section title={t.live.todayHeading} fixtures={groups.today} />
|
||||
{groups.upcoming.map((g, i) => (
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggle(team)}
|
||||
aria-label={active ? t.team.unfollow : t.team.follow}
|
||||
aria-pressed={active}
|
||||
className={`grid h-9 w-9 place-items-center rounded-md transition-colors ${active ? 'text-gold' : 'text-faint hover:text-ink'}`}
|
||||
>
|
||||
<Star size={20} fill={active ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function TeamProfilePage() {
|
||||
const { name } = useParams({ strict: false }) as { name: string };
|
||||
const t = useT();
|
||||
@@ -62,7 +81,10 @@ export function TeamProfilePage() {
|
||||
<CardBody className="flex flex-wrap items-center gap-4">
|
||||
<span aria-hidden className="text-5xl">{teamFlag(profile.team)}</span>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-extrabold text-ink">{profile.team}</h1>
|
||||
<h1 className="flex items-center gap-2 font-display text-2xl font-extrabold text-ink">
|
||||
{profile.team}
|
||||
<FavoriteStar team={profile.team} />
|
||||
</h1>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm">
|
||||
{profile.group && <Badge tone="neutral">{fmt(t.common.group, { group: profile.group })}</Badge>}
|
||||
<Badge tone="muted">{fmt(t.team.eloBadge, { rating: profile.elo })}</Badge>
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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<FavoriteState>((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 });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user