// DEPLOY_CONFIG: {"triggers": [{"name": "send_league_summary_on_game_end", "on": "game.end", "actions": [{"type": "email", "to": "league-admins@deplixo.app", "subject": "League Summary: Game Ended", "body": "A game has ended. Please use the stored game history and standings to compile and review the latest league summary."}]}]} import { useEffect, useMemo, useRef, useState } from 'react'; import { useBroadcast, useCollection, useIdentity, usePresence, useReactions, playSound, share } from '@deplixo/sdk'; import { AdminPanel } from './components/AdminPanel.jsx'; import { Scoreboard } from './components/Scoreboard.jsx'; import { PlayByPlay } from './components/PlayByPlay.jsx'; // PROGRESS:sc_001:complete:Setting up the live scoreboard shell function App() { const { user, loading } = useIdentity(); const [role, setRole] = useState('viewer'); const [activeTab, setActiveTab] = useState('scoreboard'); const [shareStatus, setShareStatus] = useState(''); const lastScoringEventRef = useRef(null); const lastProcessedGameRef = useRef(null); const { items: historyItems, add: addGameHistory, loading: historyLoading } = useCollection('game_history'); const { items: standingsItems, add: addStanding, update: updateStanding, loading: standingsLoading } = useCollection('season_standings'); const presenceUserData = { id: user?.id || user?.name || 'anonymous', name: user?.name || 'Guest', role, }; const { users } = usePresence(presenceUserData); const viewerCount = (users || []).filter((presenceUser) => presenceUser?.role !== 'admin').length; const displayRole = role === 'admin' ? 'Admin' : 'Viewer'; const getScoreboardShareUrl = () => { if (typeof window === 'undefined') return ''; const url = new URL(window.location.href); url.searchParams.set('tab', 'scoreboard'); url.hash = 'scoreboard'; return url.toString(); }; const handleShareScoreboard = async () => { const shareUrl = getScoreboardShareUrl(); if (!shareUrl) return; const shareData = { title: 'League Live Scoreboard', text: 'Check out the current League Live scoreboard.', url: shareUrl, }; try { if (navigator?.share) { await navigator.share(shareData); setShareStatus('Shared'); return; } if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(shareUrl); setShareStatus('Link copied'); return; } setShareStatus('Sharing unavailable'); } catch (error) { try { if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(shareUrl); setShareStatus('Link copied'); return; } } catch (clipboardError) { setShareStatus('Share failed'); } } finally { window.setTimeout(() => setShareStatus(''), 2500); } }; const handleScoreEvent = useMemo(() => { return async (event = {}) => { const eventType = String(event?.type || event?.eventType || event?.kind || '').toLowerCase(); const pointsValue = Number(event?.points ?? event?.delta ?? event?.scoreChange ?? 0); const isRelevantScoreEvent = eventType.includes('goal') || eventType.includes('score') || eventType.includes('points') || eventType.includes('point') || pointsValue > 0; if (!isRelevantScoreEvent) return; const eventKey = event?.id || `${event?.gameId || 'game'}:${event?.teamId || 'team'}:${event?.timestamp || event?.createdAt || Date.now()}`; if (lastScoringEventRef.current === eventKey) return; lastScoringEventRef.current = eventKey; playSound(pointsValue >= 3 ? '@success' : '@ding'); const gameId = event?.gameId || event?.matchId || 'current-game'; const seasonId = event?.seasonId || event?.season || 'current-season'; const teamId = event?.teamId || event?.team || 'team'; const opponentId = event?.opponentId || event?.opponent || null; const teamName = event?.teamName || event?.team || 'Team'; const opponentName = event?.opponentName || event?.opponent || 'Opponent'; const won = Boolean(event?.winner || event?.result === 'win' || event?.outcome === 'win'); const lost = Boolean(event?.loser || event?.result === 'loss' || event?.outcome === 'loss'); const tied = Boolean(event?.result === 'tie' || event?.outcome === 'tie' || event?.draw); const result = won ? 'win' : lost ? 'loss' : tied ? 'tie' : 'scored'; const gameRecordId = event?.historyId || event?.recordId || eventKey; if (lastProcessedGameRef.current !== gameRecordId) { lastProcessedGameRef.current = gameRecordId; const gameHistoryRecord = { id: gameRecordId, gameId, seasonId, teamId, teamName, opponentId, opponentName, points: pointsValue, result, eventType, timestamp: event?.timestamp || event?.createdAt || new Date().toISOString(), source: event?.source || 'broadcast', }; await addGameHistory(gameHistoryRecord); const existingStanding = (standingsItems || []).find( (item) => item?.seasonId === seasonId && item?.teamId === teamId ); const nextPlayed = Number(existingStanding?.played || 0) + 1; const nextWins = Number(existingStanding?.wins || 0) + (won ? 1 : 0); const nextLosses = Number(existingStanding?.losses || 0) + (lost ? 1 : 0); const nextTies = Number(existingStanding?.ties || 0) + (tied ? 1 : 0); const nextPointsFor = Number(existingStanding?.pointsFor || 0) + pointsValue; const nextPointsAgainst = Number(existingStanding?.pointsAgainst || 0) + Number(event?.opponentPoints || 0); const nextDiff = nextPointsFor - nextPointsAgainst; const nextStandPoints = nextWins * 3 + nextTies; const standingPayload = { seasonId, teamId, teamName, played: nextPlayed, wins: nextWins, losses: nextLosses, ties: nextTies, pointsFor: nextPointsFor, pointsAgainst: nextPointsAgainst, pointDiff: nextDiff, standingsPoints: nextStandPoints, lastGameAt: gameHistoryRecord.timestamp, }; if (existingStanding?.id) { await updateStanding(existingStanding.id, standingPayload); } else { await addStanding(standingPayload); } } }; }, [addGameHistory, addStanding, standingsItems, updateStanding]); useBroadcast('score:update', handleScoreEvent); useBroadcast('game:score:update', handleScoreEvent); useBroadcast('goal:scored', handleScoreEvent); useBroadcast('play:created', async (event = {}) => { const eventType = String(event?.type || event?.eventType || event?.kind || '').toLowerCase(); const pointsValue = Number(event?.points ?? event?.delta ?? event?.scoreChange ?? 0); const isScoringPlay = eventType.includes('goal') || eventType.includes('score') || eventType.includes('points') || pointsValue > 0; if (!isScoringPlay) return; const eventKey = event?.id || `${event?.gameId || 'game'}:${event?.teamId || 'team'}:${event?.timestamp || event?.createdAt || Date.now()}`; if (lastScoringEventRef.current === eventKey) return; lastScoringEventRef.current = eventKey; playSound(pointsValue >= 3 ? '@success' : '@ding'); const gameId = event?.gameId || event?.matchId || 'current-game'; const seasonId = event?.seasonId || event?.season || 'current-season'; const teamId = event?.teamId || event?.team || 'team'; const opponentId = event?.opponentId || event?.opponent || null; const teamName = event?.teamName || event?.team || 'Team'; const opponentName = event?.opponentName || event?.opponent || 'Opponent'; const won = Boolean(event?.winner || event?.result === 'win' || event?.outcome === 'win'); const lost = Boolean(event?.loser || event?.result === 'loss' || event?.outcome === 'loss'); const tied = Boolean(event?.result === 'tie' || event?.outcome === 'tie' || event?.draw); const result = won ? 'win' : lost ? 'loss' : tied ? 'tie' : 'scored'; const gameRecordId = event?.historyId || event?.recordId || eventKey; if (lastProcessedGameRef.current !== gameRecordId) { lastProcessedGameRef.current = gameRecordId; const gameHistoryRecord = { id: gameRecordId, gameId, seasonId, teamId, teamName, opponentId, opponentName, points: pointsValue, result, eventType, timestamp: event?.timestamp || event?.createdAt || new Date().toISOString(), source: event?.source || 'broadcast', }; await addGameHistory(gameHistoryRecord); const existingStanding = (standingsItems || []).find( (item) => item?.seasonId === seasonId && item?.teamId === teamId ); const nextPlayed = Number(existingStanding?.played || 0) + 1; const nextWins = Number(existingStanding?.wins || 0) + (won ? 1 : 0); const nextLosses = Number(existingStanding?.losses || 0) + (lost ? 1 : 0); const nextTies = Number(existingStanding?.ties || 0) + (tied ? 1 : 0); const nextPointsFor = Number(existingStanding?.pointsFor || 0) + pointsValue; const nextPointsAgainst = Number(existingStanding?.pointsAgainst || 0) + Number(event?.opponentPoints || 0); const nextDiff = nextPointsFor - nextPointsAgainst; const nextStandPoints = nextWins * 3 + nextTies; const standingPayload = { seasonId, teamId, teamName, played: nextPlayed, wins: nextWins, losses: nextLosses, ties: nextTies, pointsFor: nextPointsFor, pointsAgainst: nextPointsAgainst, pointDiff: nextDiff, standingsPoints: nextStandPoints, lastGameAt: gameHistoryRecord.timestamp, }; if (existingStanding?.id) { await updateStanding(existingStanding.id, standingPayload); } else { await addStanding(standingPayload); } } }); if (loading || historyLoading || standingsLoading) { return (
Loading scoreboard…
Loading historical results…
No historical game records yet.
) : (Loading standings…
No standings recorded yet.
) : (Loading charts…
No season data available yet.
) : ({emptyLabel}
; } return ({emptyLabel}
; } return (