brackt/app/components/scoring/MatchSchedule.tsx

133 lines
5 KiB
TypeScript
Raw Normal View History

feat: add match sync service, cron job, public tournament display (Phases 2-4) Phase 2 — Match Sync Adapters + Cron Job: - PandaScoreMatchSyncAdapter (CS2): fetches matches with map-level sub-games, stage detection - EspnScheduleAdapter (MLB/NBA/MLS/WNBA/NHL): fetches scoreboard with live scores - syncMatches() orchestrator: resolves participants by externalId+name, bulk-upserts season_matches, syncs playoff bracket results through existing setMatchWinner/processMatchResult pipeline - POST /admin/jobs/sync-matches cron endpoint (mirrors sync-and-simulate pattern) - External Season ID field added to sports season create/edit admin forms - Sync from API button wired in CS2 setup page (enabled when externalSeasonId set) Phase 3 — Public Tournament & Schedule Display: - MatchSchedule component: generic match list with live/scheduled/complete status badges, matchday grouping - Cs2TournamentBracket component: tab layout (Opening/Challengers/Legends/Champions Stage), map scores per match - /sports-seasons/:sportsSeasonId/tournament public route with 30-second live polling Phase 4 — Playoff Bracket Auto-Sync: - externalMatchId column added to playoff_matches table (migration 0121) - Bracket matches (matchStage=null) auto-synced: matches existing playoff_match rows by externalMatchId then participant IDs, calls full scoring pipeline - autoCompleteRoundIfDone extracted to scoring-calculator.ts for shared use All 2365 tests pass; typecheck clean. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
2026-06-12 20:46:30 +00:00
import type { SeasonMatch, MatchSubGame } from "~/models/season-match";
type MatchWithRelations = SeasonMatch & {
participant1: { name: string } | null;
participant2: { name: string } | null;
winner: { name: string } | null;
subGames: MatchSubGame[];
};
interface MatchScheduleProps {
matches: MatchWithRelations[];
title?: string;
}
const STATUS_BADGE: Record<string, string> = {
scheduled: "bg-muted text-muted-foreground",
in_progress: "bg-amber-500/15 text-amber-400 border border-amber-500/30",
complete: "bg-emerald-500/15 text-emerald-400 border border-emerald-500/30",
canceled: "bg-destructive/15 text-destructive border border-destructive/30",
postponed: "bg-muted text-muted-foreground",
};
const STATUS_LABEL: Record<string, string> = {
scheduled: "Scheduled",
in_progress: "Live",
complete: "Final",
canceled: "Canceled",
postponed: "Postponed",
};
function formatDate(d: Date | null) {
if (!d) return "TBD";
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
timeZoneName: "short",
}).format(new Date(d));
}
function MatchRow({ match }: { match: MatchWithRelations }) {
const p1 = match.participant1?.name ?? "TBD";
const p2 = match.participant2?.name ?? "TBD";
const isComplete = match.status === "complete";
const winnerId = match.winnerId;
return (
<div className="flex items-center justify-between py-3 px-4 border-b border-border last:border-0">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3">
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium truncate ${isComplete && winnerId === match.participant1Id ? "text-foreground" : isComplete ? "text-muted-foreground" : "text-foreground"}`}>
{p1}
</div>
<div className={`text-sm font-medium truncate ${isComplete && winnerId === match.participant2Id ? "text-foreground" : isComplete ? "text-muted-foreground" : "text-foreground"}`}>
{p2}
</div>
</div>
{isComplete && (
<div className="text-right tabular-nums shrink-0">
<div className={`text-sm font-bold ${winnerId === match.participant1Id ? "text-foreground" : "text-muted-foreground"}`}>
{match.participant1Score ?? "-"}
</div>
<div className={`text-sm font-bold ${winnerId === match.participant2Id ? "text-foreground" : "text-muted-foreground"}`}>
{match.participant2Score ?? "-"}
</div>
</div>
)}
{match.status === "in_progress" && (
<div className="text-right tabular-nums shrink-0">
<div className="text-sm font-bold text-amber-400">{match.participant1Score ?? "-"}</div>
<div className="text-sm font-bold text-amber-400">{match.participant2Score ?? "-"}</div>
</div>
)}
</div>
{match.matchday != null && (
<div className="text-xs text-muted-foreground mt-1">Matchday {match.matchday}</div>
)}
</div>
<div className="ml-4 flex flex-col items-end gap-1 shrink-0">
<span className={`text-xs px-2 py-0.5 rounded-full ${STATUS_BADGE[match.status] ?? STATUS_BADGE.scheduled}`}>
{match.status === "in_progress" && <span className="inline-block w-1.5 h-1.5 rounded-full bg-amber-400 mr-1 animate-pulse" />}
{STATUS_LABEL[match.status] ?? match.status}
</span>
<span className="text-xs text-muted-foreground">{formatDate(match.scheduledAt)}</span>
</div>
</div>
);
}
// Groups by matchday number when available, or by UTC calendar date (YYYY-MM-DD) for date-based sports.
// UTC date keys ensure consistent grouping regardless of the viewer's timezone.
feat: add match sync service, cron job, public tournament display (Phases 2-4) Phase 2 — Match Sync Adapters + Cron Job: - PandaScoreMatchSyncAdapter (CS2): fetches matches with map-level sub-games, stage detection - EspnScheduleAdapter (MLB/NBA/MLS/WNBA/NHL): fetches scoreboard with live scores - syncMatches() orchestrator: resolves participants by externalId+name, bulk-upserts season_matches, syncs playoff bracket results through existing setMatchWinner/processMatchResult pipeline - POST /admin/jobs/sync-matches cron endpoint (mirrors sync-and-simulate pattern) - External Season ID field added to sports season create/edit admin forms - Sync from API button wired in CS2 setup page (enabled when externalSeasonId set) Phase 3 — Public Tournament & Schedule Display: - MatchSchedule component: generic match list with live/scheduled/complete status badges, matchday grouping - Cs2TournamentBracket component: tab layout (Opening/Challengers/Legends/Champions Stage), map scores per match - /sports-seasons/:sportsSeasonId/tournament public route with 30-second live polling Phase 4 — Playoff Bracket Auto-Sync: - externalMatchId column added to playoff_matches table (migration 0121) - Bracket matches (matchStage=null) auto-synced: matches existing playoff_match rows by externalMatchId then participant IDs, calls full scoring pipeline - autoCompleteRoundIfDone extracted to scoring-calculator.ts for shared use All 2365 tests pass; typecheck clean. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
2026-06-12 20:46:30 +00:00
function groupByMatchday(matches: MatchWithRelations[]) {
const groups = new Map<string, MatchWithRelations[]>();
for (const m of matches) {
const key = m.matchday != null
? `Matchday ${m.matchday}`
: m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD";
feat: add match sync service, cron job, public tournament display (Phases 2-4) Phase 2 — Match Sync Adapters + Cron Job: - PandaScoreMatchSyncAdapter (CS2): fetches matches with map-level sub-games, stage detection - EspnScheduleAdapter (MLB/NBA/MLS/WNBA/NHL): fetches scoreboard with live scores - syncMatches() orchestrator: resolves participants by externalId+name, bulk-upserts season_matches, syncs playoff bracket results through existing setMatchWinner/processMatchResult pipeline - POST /admin/jobs/sync-matches cron endpoint (mirrors sync-and-simulate pattern) - External Season ID field added to sports season create/edit admin forms - Sync from API button wired in CS2 setup page (enabled when externalSeasonId set) Phase 3 — Public Tournament & Schedule Display: - MatchSchedule component: generic match list with live/scheduled/complete status badges, matchday grouping - Cs2TournamentBracket component: tab layout (Opening/Challengers/Legends/Champions Stage), map scores per match - /sports-seasons/:sportsSeasonId/tournament public route with 30-second live polling Phase 4 — Playoff Bracket Auto-Sync: - externalMatchId column added to playoff_matches table (migration 0121) - Bracket matches (matchStage=null) auto-synced: matches existing playoff_match rows by externalMatchId then participant IDs, calls full scoring pipeline - autoCompleteRoundIfDone extracted to scoring-calculator.ts for shared use All 2365 tests pass; typecheck clean. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
2026-06-12 20:46:30 +00:00
if (!groups.has(key)) groups.set(key, []);
groups.get(key)!.push(m);
}
return groups;
}
export function MatchSchedule({ matches, title = "Schedule" }: MatchScheduleProps) {
if (matches.length === 0) {
return (
<div className="rounded-lg border border-border p-6 text-center text-muted-foreground text-sm">
No matches scheduled yet.
</div>
);
}
const grouped = groupByMatchday(matches);
feat: add match sync service, cron job, public tournament display (Phases 2-4) Phase 2 — Match Sync Adapters + Cron Job: - PandaScoreMatchSyncAdapter (CS2): fetches matches with map-level sub-games, stage detection - EspnScheduleAdapter (MLB/NBA/MLS/WNBA/NHL): fetches scoreboard with live scores - syncMatches() orchestrator: resolves participants by externalId+name, bulk-upserts season_matches, syncs playoff bracket results through existing setMatchWinner/processMatchResult pipeline - POST /admin/jobs/sync-matches cron endpoint (mirrors sync-and-simulate pattern) - External Season ID field added to sports season create/edit admin forms - Sync from API button wired in CS2 setup page (enabled when externalSeasonId set) Phase 3 — Public Tournament & Schedule Display: - MatchSchedule component: generic match list with live/scheduled/complete status badges, matchday grouping - Cs2TournamentBracket component: tab layout (Opening/Challengers/Legends/Champions Stage), map scores per match - /sports-seasons/:sportsSeasonId/tournament public route with 30-second live polling Phase 4 — Playoff Bracket Auto-Sync: - externalMatchId column added to playoff_matches table (migration 0121) - Bracket matches (matchStage=null) auto-synced: matches existing playoff_match rows by externalMatchId then participant IDs, calls full scoring pipeline - autoCompleteRoundIfDone extracted to scoring-calculator.ts for shared use All 2365 tests pass; typecheck clean. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
2026-06-12 20:46:30 +00:00
return (
<div className="space-y-4">
{title && <h3 className="text-lg font-semibold">{title}</h3>}
{[...grouped.entries()].map(([label, group]) => (
<div key={label} className="rounded-lg border border-border overflow-hidden">
<div className="px-4 py-2 bg-muted/30 text-sm font-medium text-muted-foreground">
{label}
</div>
{group.map((m) => (
<MatchRow key={m.id} match={m} />
))}
</div>
))}
feat: add match sync service, cron job, public tournament display (Phases 2-4) Phase 2 — Match Sync Adapters + Cron Job: - PandaScoreMatchSyncAdapter (CS2): fetches matches with map-level sub-games, stage detection - EspnScheduleAdapter (MLB/NBA/MLS/WNBA/NHL): fetches scoreboard with live scores - syncMatches() orchestrator: resolves participants by externalId+name, bulk-upserts season_matches, syncs playoff bracket results through existing setMatchWinner/processMatchResult pipeline - POST /admin/jobs/sync-matches cron endpoint (mirrors sync-and-simulate pattern) - External Season ID field added to sports season create/edit admin forms - Sync from API button wired in CS2 setup page (enabled when externalSeasonId set) Phase 3 — Public Tournament & Schedule Display: - MatchSchedule component: generic match list with live/scheduled/complete status badges, matchday grouping - Cs2TournamentBracket component: tab layout (Opening/Challengers/Legends/Champions Stage), map scores per match - /sports-seasons/:sportsSeasonId/tournament public route with 30-second live polling Phase 4 — Playoff Bracket Auto-Sync: - externalMatchId column added to playoff_matches table (migration 0121) - Bracket matches (matchStage=null) auto-synced: matches existing playoff_match rows by externalMatchId then participant IDs, calls full scoring pipeline - autoCompleteRoundIfDone extracted to scoring-calculator.ts for shared use All 2365 tests pass; typecheck clean. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6
2026-06-12 20:46:30 +00:00
</div>
);
}