import { ActionIcon, Card, Flex, Text, Indicator } from "@mantine/core"; import { PlayIcon, PencilIcon, SpeakerHighIcon } from "@phosphor-icons/react"; import React, { useCallback, useMemo } from "react"; import { MatchSlot } from "./match-slot"; import { Match } from "@/features/matches/types"; import { Team } from "@/features/teams/types"; import { useSheet } from "@/hooks/use-sheet"; import { MatchForm } from "./match-form"; import Sheet from "@/components/sheet/sheet"; import { useServerMutation } from "@/lib/tanstack-query/hooks"; import { endMatch, startMatch } from "@/features/matches/server"; import { tournamentKeys } from "@/features/tournaments/queries"; import { useQueryClient } from "@tanstack/react-query"; import { useSpotifyPlayback } from "@/lib/spotify/hooks"; interface MatchCardProps { match: Match; orders: Record; showControls?: boolean; groupConfig?: { num_groups: number; advance_per_group: number; }; } export const MatchCard: React.FC = ({ match, orders, showControls, groupConfig, }) => { const queryClient = useQueryClient(); const editSheet = useSheet(); const { playTrack, pause } = useSpotifyPlayback(); const getGroupLabel = useCallback((seed: number | undefined) => { if (!seed || !groupConfig) return undefined; const groupNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; const teamsPerRank = groupConfig.num_groups; const rankIndex = Math.floor((seed - 1) / teamsPerRank); const groupIndex = (seed - 1) % teamsPerRank; const rank = rankIndex + 1; const groupName = groupNames[groupIndex] || `${groupIndex + 1}`; const rankSuffix = rank === 1 ? '1st' : rank === 2 ? '2nd' : rank === 3 ? '3rd' : `${rank}th`; return `${groupName} ${rankSuffix}`; }, [groupConfig]); const homeSlot = useMemo( () => ({ from: orders[match.home_from_lid], from_loser: match.home_from_loser, team: match.home, seed: match.home_seed, cups: match.status === "ended" ? match.home_cups : undefined, isWinner: match.status === "ended" && match.home_cups !== undefined && match.away_cups !== undefined && match.home_cups > match.away_cups, groupLabel: !match.home && match.home_seed ? getGroupLabel(match.home_seed) : undefined, }), [match, getGroupLabel] ); const awaySlot = useMemo( () => ({ from: orders[match.away_from_lid], from_loser: match.away_from_loser, team: match.away, seed: match.away_seed, cups: match.status === "ended" ? match.away_cups : undefined, isWinner: match.status === "ended" && match.away_cups !== undefined && match.home_cups !== undefined && match.away_cups > match.home_cups, groupLabel: !match.away && match.away_seed ? getGroupLabel(match.away_seed) : undefined, }), [match, getGroupLabel] ); const showToolbar = useMemo( () => match.status === "ready" && showControls, [match.status, showControls] ); const showEditButton = useMemo( () => showControls && match.status === "started", [showControls, match.status] ); const hasWalkoutData = showControls && match.home && match.away && 'song_id' in match.home && 'song_id' in match.away; const start = useServerMutation({ mutationFn: startMatch, successMessage: "Match started!", onSuccess: () => { queryClient.invalidateQueries({ queryKey: tournamentKeys.details(match.tournament.id), }); }, }); const end = useServerMutation({ mutationFn: endMatch, onSuccess: () => { queryClient.invalidateQueries({ queryKey: tournamentKeys.details(match.tournament.id), }); }, }); const handleFormSubmit = useCallback( async (data: { home_cups: number; away_cups: number; ot_count: number; }) => { end.mutate({ data: { ...data, matchId: match.id, }, }); editSheet.close(); }, [match.id, editSheet] ); const speak = useCallback((text: string): Promise => { return new Promise((resolve) => { if (!("speechSynthesis" in window)) { resolve(); return; } const utterance = new SpeechSynthesisUtterance(text); const voices = window.speechSynthesis.getVoices(); const preferredVoice = voices.find( (voice) => voice.lang.startsWith("en") && voice.name.includes("Daniel") ) || voices.find((voice) => voice.lang.startsWith("en") && voice.default); if (preferredVoice) { utterance.voice = preferredVoice; } utterance.rate = 0.9; utterance.volume = 0.8; utterance.pitch = 1.0; utterance.onend = () => resolve(); utterance.onerror = () => resolve(); window.speechSynthesis.speak(utterance); }); }, []); const playTeamWalkout = useCallback((team: Team): Promise => { return new Promise((resolve) => { const songDuration = (team.song_end - team.song_start) * 1000; playTrack(team.song_id, undefined, team.song_start * 1000); setTimeout(async () => { await pause(); resolve(); }, songDuration); }); }, [playTrack, pause]); const handleSpeakerClick = useCallback(async () => { if (!hasWalkoutData || !match.home?.name || !match.away?.name) { await speak(`${match.home?.name || "Home"} vs. ${match.away?.name || "Away"}`); return; } try { const homeTeam = match.home as Team; const awayTeam = match.away as Team; await playTeamWalkout(homeTeam); await speak(homeTeam.name); await speak("versus"); await playTeamWalkout(awayTeam); await speak(awayTeam.name); await speak("have fun, good luck!"); } catch (error) { console.warn('Walkout sequence error:', error); await speak(`${match.home.name} vs. ${match.away.name}`); } }, [hasWalkoutData, match.home, match.away, speak, playTeamWalkout]); const handleStart = useCallback(async () => { start.mutate({ data: match.id, }); // Skip announcements for regional tournaments const isRegional = match.tournament?.regional === true; // Play walkout sequence after starting the match (only for non-regional tournaments) if (!isRegional && hasWalkoutData && match.home?.name && match.away?.name) { try { const homeTeam = match.home as Team; const awayTeam = match.away as Team; await playTeamWalkout(homeTeam); await speak(homeTeam.name); await speak("versus"); await playTeamWalkout(awayTeam); await speak(awayTeam.name); await speak("have fun, good luck!"); } catch (error) { console.warn('Auto-walkout sequence error:', error); } } }, [match, start, hasWalkoutData, playTeamWalkout, speak]); return ( {match.order} {match.reset && ( * If necessary )} {showControls && match.status !== "tbd" && ( )} {showToolbar && ( )} {showEditButton && ( )} ); };