Files
2026FIFAWorldCup/platform/web/app/matches/page.tsx
QuantBot aa7e3bba76
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality & Testing (push) Failing after 1m49s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Rsync (push) Has been skipped
chore: migrate deployment to Gitea Actions with zero-trust rsync
2026-06-16 19:06:50 +08:00

84 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { formatToTaipeiTime } from '@/lib/timezone';
import { getAllMatches, type MatchListItem } from '@/lib/analytics-api';
import { matchStatusLabel, sortMatchesForProfessionalDisplay } from '@/lib/match-order';
function scoreLine(match: MatchListItem): string {
if (typeof match.home_score === 'number' && typeof match.away_score === 'number') {
return `${match.home_team} ${match.home_score} - ${match.away_score} ${match.away_team}`;
}
return `${match.home_team} vs ${match.away_team}`;
}
export default function MatchesPage() {
const [matches, setMatches] = useState<MatchListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadMatches() {
try {
const data = await getAllMatches();
setMatches(sortMatchesForProfessionalDisplay(data || []));
setError(null);
} catch (error) {
console.error('Failed to load matches', error);
setError('賽事資料暫時無法同步,系統正在等待資料源回補。請先到資料健康頁確認目前狀態。');
} finally {
setLoading(false);
}
}
loadMatches();
const interval = window.setInterval(loadMatches, 60_000);
return () => window.clearInterval(interval);
}, []);
if (loading) {
return <div className="p-8 text-[#8a6b58] dot-matrix">...</div>;
}
if (matches.length === 0) {
return (
<div className="p-8 text-[#7d2a15] dot-matrix">
{error || '目前沒有可顯示的賽事資料。'}
</div>
);
}
return (
<div className="space-y-4">
<h2 className="dot-matrix text-2xl text-[#7d2a15]"></h2>
<section className="grid gap-4">
{matches.map((match) => (
<Link
key={match.match_id}
href={`/matches/${encodeURIComponent(match.match_id)}`}
className="panel-glow block rounded-2xl p-5 transition hover:-translate-y-0.5 hover:border-[#c58b63] hover:bg-[#fff8e6]"
>
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<p className="text-lg font-semibold text-[#6b3f2d]">{scoreLine(match)}</p>
<span className="w-fit rounded-full bg-[#fff4df] px-3 py-1 text-xs font-semibold text-[#7d2a15] ring-1 ring-[#e8cead]">
{matchStatusLabel(match)}
</span>
</div>
<p className="mt-3 text-sm text-[#8a6b58]">{formatToTaipeiTime(match.kickoff_utc)}</p>
<p className="text-sm text-[#7c5340]">
{match.venue_name || '未定'} ({match.venue_city || '未定'})
</p>
<p className="mt-3 text-xs font-semibold text-[#b83822]"> </p>
</Link>
))}
</section>
<section className="panel-glow rounded-2xl border-dashed p-5">
<h3 className="dot-matrix text-xl text-[#7d2a15]"></h3>
<p className="mt-2 text-sm leading-6 text-[#7a5b46]">
OptaFIFA
</p>
</section>
</div>
);
}