Mobile overhaul: round-picker bracket, 5-tab nav + More sheet, real icons
- Bracket on phones: a segmented round picker with full-width cards replaces the 1040px sideways scroll; desktop keeps the columns. - Odds table: sticky team column; the three lowest-value columns hide on phones so Team/QF/SF/Final/Champion fit without scrolling. - Bottom nav: 4 primary tabs + a More sheet (vs Markt, Modell, Story, Teams, Daten, plus language & theme toggles) — 7 tabs were cramped. - Emoji → icons: timeline events use a ball icon, card-shaped color chips and substitution arrows; unknown-team fallbacks use a shield. Country-flag emojis stay (intentional). - Readability: bigger scores on match cards, minimum text sizes bumped, probability-number triple hides on the narrowest screens (the bar carries the information). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+87
-21
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Gauge, GitMerge, Languages, Moon, Radio, Scale, Sparkles, Sun, Table, TrendingUp, Trophy } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, Outlet, useRouterState } from '@tanstack/react-router';
|
||||
import { Database, Gauge, GitMerge, Languages, Menu, Moon, Radio, Scale, Sparkles, Sun, Table, TrendingUp, Trophy, Users } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
import { useLocaleStore } from '@/stores/localeStore';
|
||||
@@ -8,7 +8,9 @@ import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
import { useT, type Dict } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const NAV: { to: string; label: (t: Dict) => string; exact: boolean; icon: LucideIcon }[] = [
|
||||
interface NavItem { to: string; label: (t: Dict) => string; exact: boolean; icon: LucideIcon }
|
||||
|
||||
const NAV: NavItem[] = [
|
||||
{ to: '/', label: (t) => t.nav.live, exact: true, icon: Radio },
|
||||
{ to: '/groups', label: (t) => t.nav.groups, exact: false, icon: Table },
|
||||
{ to: '/bracket', label: (t) => t.nav.bracket, exact: false, icon: GitMerge },
|
||||
@@ -18,6 +20,14 @@ const NAV: { to: string; label: (t: Dict) => string; exact: boolean; icon: Lucid
|
||||
{ to: '/story', label: (t) => t.nav.story, exact: false, icon: Sparkles },
|
||||
];
|
||||
|
||||
// Phones get four primary tabs + a "More" sheet (7 tabs were unusably cramped).
|
||||
const MOBILE_PRIMARY: NavItem[] = NAV.slice(0, 4);
|
||||
const MOBILE_MORE: NavItem[] = [
|
||||
...NAV.slice(4),
|
||||
{ to: '/teams', label: (t) => t.nav.teams, exact: false, icon: Users },
|
||||
{ to: '/data', label: (t) => t.nav.data, exact: false, icon: Database },
|
||||
];
|
||||
|
||||
function ConnectionStatus() {
|
||||
const t = useT();
|
||||
const status = useTournamentStore((s) => s.status);
|
||||
@@ -76,6 +86,78 @@ export function LanguageToggle() {
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNav() {
|
||||
const t = useT();
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
||||
const moreActive = MOBILE_MORE.some((i) => pathname.startsWith(i.to));
|
||||
|
||||
useEffect(() => setMoreOpen(false), [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{moreOpen && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t.common.back}
|
||||
className="fixed inset-0 z-30 bg-black/40 sm:hidden"
|
||||
onClick={() => setMoreOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{moreOpen && (
|
||||
<div className="fixed inset-x-0 bottom-[52px] z-40 rounded-t-2xl border-t border-line bg-surface p-3 pb-4 shadow-2xl sm:hidden">
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{MOBILE_MORE.map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={cn(
|
||||
'flex min-h-11 items-center gap-2.5 rounded-lg px-3 py-2.5 text-sm font-medium text-muted hover:bg-elevated hover:text-ink',
|
||||
'[&.active]:bg-accent-glow [&.active]:text-accent',
|
||||
)}
|
||||
>
|
||||
<item.icon size={17} />
|
||||
{item.label(t)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-end gap-1.5 border-t border-line pt-2">
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<nav className="fixed inset-x-0 bottom-0 z-40 flex border-t border-line bg-surface/95 backdrop-blur sm:hidden">
|
||||
{MOBILE_PRIMARY.map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
{...(item.exact ? { activeOptions: { exact: true } } : {})}
|
||||
className={cn(
|
||||
'flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 py-1.5 text-[11px] font-medium text-muted',
|
||||
'[&.active]:text-accent',
|
||||
)}
|
||||
>
|
||||
<item.icon size={18} />
|
||||
{item.label(t)}
|
||||
</Link>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMoreOpen((v) => !v)}
|
||||
className={cn(
|
||||
'flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 py-1.5 text-[11px] font-medium',
|
||||
moreOpen || moreActive ? 'text-accent' : 'text-muted',
|
||||
)}
|
||||
>
|
||||
<Menu size={18} />
|
||||
{t.nav.more}
|
||||
</button>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RootLayout() {
|
||||
const t = useT();
|
||||
useEffect(() => {
|
||||
@@ -119,23 +201,7 @@ export function RootLayout() {
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
{/* Mobile bottom tab bar */}
|
||||
<nav className="fixed inset-x-0 bottom-0 z-30 flex border-t border-line bg-surface/95 backdrop-blur sm:hidden">
|
||||
{NAV.map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
{...(item.exact ? { activeOptions: { exact: true } } : {})}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center gap-0.5 py-2 text-[10px] font-medium text-muted',
|
||||
'[&.active]:text-accent',
|
||||
)}
|
||||
>
|
||||
<item.icon size={18} />
|
||||
{item.label(t)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<MobileNav />
|
||||
|
||||
<footer className="border-t border-line py-6 pb-24 text-center text-xs text-faint sm:pb-6">
|
||||
<p>
|
||||
|
||||
@@ -65,7 +65,7 @@ export function MatchCard({ f }: { f: Fixture }) {
|
||||
<TeamLabel slot={f.home} align="right" />
|
||||
<div className="min-w-[64px] text-center">
|
||||
{hasScore ? (
|
||||
<div className="tnum text-xl font-bold text-ink">
|
||||
<div className="tnum text-2xl font-bold text-ink">
|
||||
{f.homeScore ?? 0}<span className="px-1.5 text-faint">–</span>{f.awayScore ?? 0}
|
||||
</div>
|
||||
) : (
|
||||
@@ -75,7 +75,7 @@ export function MatchCard({ f }: { f: Fixture }) {
|
||||
<TeamLabel slot={f.away} align="left" />
|
||||
</div>
|
||||
{f.status === 'scheduled' && <ModelHint num={f.num} />}
|
||||
<div className="mt-1.5 text-center text-[11px] text-faint">{f.venue}</div>
|
||||
<div className="mt-1.5 truncate text-center text-xs text-faint">{f.venue}</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,11 +17,18 @@ const colKeys = [
|
||||
|
||||
const pct = (x: number) => (x >= 0.995 ? '>99%' : x < 0.005 ? '—' : `${Math.round(x * 100)}%`);
|
||||
|
||||
// Lower-value columns make way on phones; Team/QF/SF/Final/Champion stay.
|
||||
const HIDE_ON_PHONE = new Set<(typeof colKeys)[number]>(['winGroup', 'qualify', 'reachR16']);
|
||||
|
||||
/** Heat-shaded cell: stronger odds → more accent (or gold for the title) tint. */
|
||||
function Cell({ x, accent = false }: { x: number; accent?: boolean }) {
|
||||
function Cell({ x, accent = false, hideOnPhone = false }: { x: number; accent?: boolean; hideOnPhone?: boolean }) {
|
||||
return (
|
||||
<td
|
||||
className={cn('py-1.5 text-center text-sm tabular-nums', x < 0.005 ? 'text-faint' : 'text-ink')}
|
||||
className={cn(
|
||||
'py-1.5 text-center text-sm tabular-nums',
|
||||
x < 0.005 ? 'text-faint' : 'text-ink',
|
||||
hideOnPhone && 'hidden sm:table-cell',
|
||||
)}
|
||||
style={x >= 0.005 ? { background: `color-mix(in oklab, var(--app-${accent ? 'gold' : 'accent'}) ${Math.round(x * 60)}%, transparent)` } : undefined}
|
||||
>
|
||||
{pct(x)}
|
||||
@@ -36,23 +43,25 @@ export function OddsTable({ odds }: { odds: TeamOdds[] }) {
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-line text-[11px] uppercase tracking-wide text-faint">
|
||||
<th className="px-3 py-2 text-left font-semibold">{t.predict.oddsTable.team}</th>
|
||||
<th className="sticky left-0 z-10 bg-panel px-3 py-2 text-left font-semibold">{t.predict.oddsTable.team}</th>
|
||||
{colKeys.map((key) => (
|
||||
<th key={key} className="px-2 py-2 text-center font-semibold">{t.predict.oddsTable[key]}</th>
|
||||
<th key={key} className={cn('px-2 py-2 text-center font-semibold', HIDE_ON_PHONE.has(key) && 'hidden sm:table-cell')}>
|
||||
{t.predict.oddsTable[key]}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{odds.map((o) => (
|
||||
<tr key={o.team} className="border-t border-line/60">
|
||||
<td className="px-3 py-1.5">
|
||||
<td className="sticky left-0 z-10 bg-panel px-3 py-1.5">
|
||||
<Link to="/team/$name" params={{ name: o.team }} className="flex items-center gap-2 hover:text-accent">
|
||||
<span aria-hidden className="text-base leading-none">{teamFlag(o.team)}</span>
|
||||
<span className="truncate text-sm font-medium text-ink">{o.team}</span>
|
||||
</Link>
|
||||
</td>
|
||||
{colKeys.map((key) => (
|
||||
<Cell key={key} x={o[key]} accent={key === 'champion'} />
|
||||
<Cell key={key} x={o[key]} accent={key === 'champion'} hideOnPhone={HIDE_ON_PHONE.has(key)} />
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { PageHeader } from '@/components/ui/PageHeader';
|
||||
import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
@@ -7,6 +7,8 @@ import { cn } from '@/lib/cn';
|
||||
import { fmt, useFormat, useT } from '@/lib/i18n';
|
||||
import type { Fixture, MatchProbs, Stage, TeamSlot } from '@/lib/types';
|
||||
|
||||
type KnockoutStage = (typeof COLUMN_STAGES)[number];
|
||||
|
||||
const COLUMN_STAGES = ['r32', 'r16', 'qf', 'sf', 'final'] as const;
|
||||
|
||||
function Side({ slot, score, winner }: { slot: TeamSlot; score: number | null; winner: boolean }) {
|
||||
@@ -43,17 +45,24 @@ function AdvanceBar({ probs }: { probs: MatchProbs }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BracketMatch({ f, probs }: { f: Fixture; probs?: MatchProbs | undefined }) {
|
||||
function BracketMatch({ f, probs, wide = false }: { f: Fixture; probs?: MatchProbs | undefined; wide?: boolean }) {
|
||||
const { t, kickoffDay, kickoffTime } = useFormat();
|
||||
const decided = f.status === 'finished' && f.homeScore !== null && f.awayScore !== null;
|
||||
const showProbs = !!probs && !!f.home.team && !!f.away.team && f.status !== 'finished';
|
||||
return (
|
||||
<Link to="/match/$num" params={{ num: String(f.num) }} className="block w-52 overflow-hidden rounded-lg border border-line bg-panel text-sm transition-colors hover:border-line-strong">
|
||||
<Link
|
||||
to="/match/$num"
|
||||
params={{ num: String(f.num) }}
|
||||
className={cn(
|
||||
'block overflow-hidden rounded-lg border border-line bg-panel text-sm transition-colors hover:border-line-strong',
|
||||
wide ? 'w-full' : 'w-52',
|
||||
)}
|
||||
>
|
||||
<Side slot={f.home} score={f.homeScore} winner={decided && f.homeScore! > f.awayScore!} />
|
||||
<div className="h-px bg-line" />
|
||||
<Side slot={f.away} score={f.awayScore} winner={decided && f.awayScore! > f.homeScore!} />
|
||||
{showProbs && <AdvanceBar probs={probs} />}
|
||||
<div className="border-t border-line bg-surface-2 px-2.5 py-1 text-[10px] text-faint">
|
||||
<div className="border-t border-line bg-surface-2 px-2.5 py-1 text-[11px] text-faint">
|
||||
{fmt(t.common.matchNumberShort, { num: f.num })} · {f.status === 'finished' ? t.common.fullTimeShort : `${kickoffDay(f.kickoff)} ${kickoffTime(f.kickoff)}`}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -79,29 +88,65 @@ export function BracketPage() {
|
||||
return { byStage, third: byStage.third[0] };
|
||||
}, [snapshot]);
|
||||
|
||||
const [mobileStage, setMobileStage] = useState<KnockoutStage>('r32');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={t.bracket.title} subtitle={t.bracket.subtitle} />
|
||||
|
||||
<div className="overflow-x-auto pb-4">
|
||||
<div className="flex gap-5">
|
||||
{/* Mobile: one round at a time behind a segmented picker — no sideways scroll. */}
|
||||
<div className="md:hidden">
|
||||
<div className="mb-4 flex overflow-hidden rounded-lg border border-line text-xs font-bold">
|
||||
{COLUMN_STAGES.map((stage) => (
|
||||
<div key={stage} className="flex shrink-0 flex-col gap-3">
|
||||
<h2 className="smallcaps">{t.stage[stage]}</h2>
|
||||
{byStage[stage].length
|
||||
? byStage[stage].map((f) => <BracketMatch key={f.num} f={f} probs={probsByNum.get(f.num)} />)
|
||||
: <div className="h-20 w-52 animate-pulse rounded-lg border border-line bg-panel" />}
|
||||
</div>
|
||||
<button
|
||||
key={stage}
|
||||
type="button"
|
||||
onClick={() => setMobileStage(stage)}
|
||||
className={cn(
|
||||
'flex-1 py-2.5 uppercase tracking-wide transition-colors',
|
||||
stage === mobileStage ? 'bg-accent-glow text-accent' : 'text-muted hover:text-ink',
|
||||
)}
|
||||
>
|
||||
{t.team.stageShort[stage]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="smallcaps mb-2">{t.stage[mobileStage]}</h2>
|
||||
<div className="flex flex-col gap-3">
|
||||
{byStage[mobileStage].length
|
||||
? byStage[mobileStage].map((f) => <BracketMatch key={f.num} f={f} probs={probsByNum.get(f.num)} wide />)
|
||||
: <div className="h-20 animate-pulse rounded-lg border border-line bg-panel" />}
|
||||
{mobileStage === 'final' && third && (
|
||||
<>
|
||||
<h2 className="smallcaps mt-3">{t.bracket.thirdPlacePlayoff}</h2>
|
||||
<BracketMatch f={third} probs={probsByNum.get(third.num)} wide />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{third && (
|
||||
<div className="mt-2">
|
||||
<h2 className="smallcaps mb-2">{t.bracket.thirdPlacePlayoff}</h2>
|
||||
<BracketMatch f={third} />
|
||||
{/* Desktop: all rounds side by side. */}
|
||||
<div className="hidden md:block">
|
||||
<div className="overflow-x-auto pb-4">
|
||||
<div className="flex gap-5">
|
||||
{COLUMN_STAGES.map((stage) => (
|
||||
<div key={stage} className="flex shrink-0 flex-col gap-3">
|
||||
<h2 className="smallcaps">{t.stage[stage]}</h2>
|
||||
{byStage[stage].length
|
||||
? byStage[stage].map((f) => <BracketMatch key={f.num} f={f} probs={probsByNum.get(f.num)} />)
|
||||
: <div className="h-20 w-52 animate-pulse rounded-lg border border-line bg-panel" />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{third && (
|
||||
<div className="mt-2">
|
||||
<h2 className="smallcaps mb-2">{t.bracket.thirdPlacePlayoff}</h2>
|
||||
<BracketMatch f={third} probs={probsByNum.get(third.num)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useParams } from '@tanstack/react-router';
|
||||
import { ArrowLeft, Shirt } from 'lucide-react';
|
||||
import { ArrowLeft, ArrowRightLeft, CircleDot, Shield, Shirt, Volleyball } from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { WinProbBar } from '@/components/WinProbBar';
|
||||
import { FormChips } from '@/components/FormChips';
|
||||
@@ -12,13 +12,15 @@ import type { Fixture, KeyPlayer, MatchPreview } from '@/lib/types';
|
||||
|
||||
const num = (s: string | undefined) => (s ? Number(s.replace('%', '')) || 0 : 0);
|
||||
|
||||
function eventIcon(type: string): string {
|
||||
/** Crisp event markers instead of emoji: ball icon for goals, card-shaped
|
||||
* color chips for bookings, arrows for substitutions. */
|
||||
function EventIcon({ type }: { type: string }) {
|
||||
const t = type.toLowerCase();
|
||||
if (t.includes('goal') || t.includes('penalty - scored')) return '⚽';
|
||||
if (t.includes('yellow')) return '🟨';
|
||||
if (t.includes('red')) return '🟥';
|
||||
if (t.includes('substitution')) return '🔁';
|
||||
return '•';
|
||||
if (t.includes('goal') || t.includes('penalty - scored')) return <Volleyball size={14} className="shrink-0 text-accent" />;
|
||||
if (t.includes('yellow')) return <span className="inline-block h-3.5 w-2.5 shrink-0 rounded-[2px] bg-amber-400" />;
|
||||
if (t.includes('red')) return <span className="inline-block h-3.5 w-2.5 shrink-0 rounded-[2px] bg-red-500" />;
|
||||
if (t.includes('substitution')) return <ArrowRightLeft size={13} className="shrink-0 text-muted" />;
|
||||
return <CircleDot size={10} className="shrink-0 text-faint" />;
|
||||
}
|
||||
|
||||
function LiveStatRow({ label, home, away }: { label: string; home: number; away: number }) {
|
||||
@@ -137,7 +139,7 @@ export function MatchPreviewPage() {
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
||||
<Link to="/team/$name" params={{ name: f.home.team ?? '' }} className="flex flex-col items-center gap-1 hover:opacity-80">
|
||||
<span aria-hidden className="text-4xl">{f.home.team ? teamFlag(f.home.team) : '⚽'}</span>
|
||||
<span aria-hidden className="grid h-10 place-items-center text-4xl">{f.home.team ? teamFlag(f.home.team) : <Shield size={32} className="text-faint" />}</span>
|
||||
<span className="text-center text-sm font-semibold text-ink">{homeName}</span>
|
||||
</Link>
|
||||
<div className="text-center">
|
||||
@@ -146,7 +148,7 @@ export function MatchPreviewPage() {
|
||||
{finished && <div className="text-[11px] font-bold uppercase text-faint">{t.common.fullTime}</div>}
|
||||
</div>
|
||||
<Link to="/team/$name" params={{ name: f.away.team ?? '' }} className="flex flex-col items-center gap-1 hover:opacity-80">
|
||||
<span aria-hidden className="text-4xl">{f.away.team ? teamFlag(f.away.team) : '⚽'}</span>
|
||||
<span aria-hidden className="grid h-10 place-items-center text-4xl">{f.away.team ? teamFlag(f.away.team) : <Shield size={32} className="text-faint" />}</span>
|
||||
<span className="text-center text-sm font-semibold text-ink">{awayName}</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -188,7 +190,7 @@ export function MatchPreviewPage() {
|
||||
{preview.events.map((e, i) => (
|
||||
<li key={i} className={`flex items-center gap-2 text-sm ${e.team === 'away' ? 'flex-row-reverse text-right' : ''}`}>
|
||||
<span className="tnum w-8 shrink-0 text-xs text-faint">{e.minute != null ? fmt(t.live.minute, { n: e.minute }) : ''}</span>
|
||||
<span aria-hidden>{eventIcon(e.type)}</span>
|
||||
<span aria-hidden className="grid w-4 shrink-0 place-items-center"><EventIcon type={e.type} /></span>
|
||||
<span className="truncate text-ink-soft">{e.text || e.type}</span>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -15,13 +15,14 @@ function ProbTriple({ label, probs, tone }: { label: string; probs: MatchProbs;
|
||||
const color = tone === 'model' ? 'bg-accent' : 'bg-gold';
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn('w-12 shrink-0 text-[10px] font-bold uppercase tracking-wide', tone === 'model' ? 'text-accent' : 'text-gold')}>{label}</span>
|
||||
<span className={cn('w-12 shrink-0 text-[11px] font-bold uppercase tracking-wide', tone === 'model' ? 'text-accent' : 'text-gold')}>{label}</span>
|
||||
<div className="flex h-2 flex-1 overflow-hidden rounded-full bg-elevated">
|
||||
<div className={color} style={{ width: pct(probs.home) }} />
|
||||
<div className="bg-line-strong" style={{ width: pct(probs.draw) }} />
|
||||
<div className="bg-info" style={{ width: pct(probs.away) }} />
|
||||
</div>
|
||||
<span className="tnum w-24 shrink-0 text-right text-[11px] text-muted">
|
||||
{/* the bar already carries the split — numbers hide on the narrowest phones */}
|
||||
<span className="tnum hidden w-24 shrink-0 text-right text-[11px] text-muted min-[420px]:inline">
|
||||
{pct(probs.home)} / {pct(probs.draw)} / {pct(probs.away)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useParams } from '@tanstack/react-router';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { ArrowLeft, Shield } from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { teamFlag } from '@/lib/teams';
|
||||
@@ -21,7 +21,7 @@ function FixtureRow({ f, team }: { f: Fixture; team: string }) {
|
||||
<Link to="/match/$num" params={{ num: String(f.num) }} className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-elevated">
|
||||
<span className="w-10 text-xs text-faint">{f.group ? fmt(t.team.groupShort, { group: f.group }) : f.stage !== 'group' ? t.team.stageShort[f.stage] : f.round}</span>
|
||||
<span className="text-faint">{isHome ? t.common.vs : t.team.awayIndicator}</span>
|
||||
<span aria-hidden>{opp.team ? teamFlag(opp.team) : '⚽'}</span>
|
||||
<span aria-hidden className="grid w-5 place-items-center">{opp.team ? teamFlag(opp.team) : <Shield size={14} className="text-faint" />}</span>
|
||||
<span className="truncate text-ink">{opp.label}</span>
|
||||
<span className="ml-auto tnum text-faint">
|
||||
{done && us != null ? <span className="text-ink">{us}–{them}</span> : `${kickoffDay(f.kickoff)} ${kickoffTime(f.kickoff)}`}
|
||||
|
||||
Reference in New Issue
Block a user