Phase 4: polish + deploy-ready — mobile nav, README, Docker validation
- Mobile bottom tab bar (icon tabs) with the top nav hidden on small screens; content padding so it never overlaps - data: scripts run via bun (not tsx/node) so the oven/bun build image needs no node — fixes the Dockerfile data:build step - README: features, data sources, dev/test/build/deploy, env vars - Validated the production Docker image end-to-end: build succeeds (378MB), container serves /healthz, /api/snapshot (104 fixtures), /api/predictions (20k sims), the SPA with deep-link fallback, and the story dataset Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+30
-11
@@ -1,17 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Moon, Sun, Trophy } from 'lucide-react';
|
||||
import { GitMerge, Moon, Radio, Sparkles, Sun, Table, TrendingUp, Trophy } from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { useUiStore } from '@/stores/uiStore';
|
||||
import { useTournamentStore } from '@/stores/tournamentStore';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const NAV = [
|
||||
{ to: '/', label: 'Live', exact: true },
|
||||
{ to: '/groups', label: 'Groups', exact: false },
|
||||
{ to: '/bracket', label: 'Bracket', exact: false },
|
||||
{ to: '/predict', label: 'Predict', exact: false },
|
||||
{ to: '/story', label: 'Story', exact: false },
|
||||
] as const;
|
||||
const NAV: { to: string; label: string; exact: boolean; icon: LucideIcon }[] = [
|
||||
{ to: '/', label: 'Live', exact: true, icon: Radio },
|
||||
{ to: '/groups', label: 'Groups', exact: false, icon: Table },
|
||||
{ to: '/bracket', label: 'Bracket', exact: false, icon: GitMerge },
|
||||
{ to: '/predict', label: 'Predict', exact: false, icon: TrendingUp },
|
||||
{ to: '/story', label: 'Story', exact: false, icon: Sparkles },
|
||||
];
|
||||
|
||||
function ConnectionStatus() {
|
||||
const status = useTournamentStore((s) => s.status);
|
||||
@@ -68,7 +69,7 @@ export function RootLayout() {
|
||||
</span>
|
||||
Cup<span className="text-accent">26</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-0.5 overflow-x-auto">
|
||||
<nav className="hidden items-center gap-0.5 sm:flex">
|
||||
{NAV.map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
@@ -90,11 +91,29 @@ export function RootLayout() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6">
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-6 pb-24 sm:pb-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-line py-6 text-center text-xs text-faint">
|
||||
{/* 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}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<footer className="border-t border-line py-6 pb-24 text-center text-xs text-faint sm:pb-6">
|
||||
<p>
|
||||
Cup26 · World Cup 2026 · data from openfootball, football-data.org & StatsBomb open data ·
|
||||
model-driven odds, not betting advice
|
||||
|
||||
Reference in New Issue
Block a user