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:
2026-06-12 00:30:48 +02:00
parent 9b34941eb2
commit dc3c375081
7 changed files with 183 additions and 60 deletions
+12 -10
View File
@@ -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>
))}