// 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…

); } return (
🏆

League Live

{user?.name || 'Guest'} · {displayRole}
{role === 'admin' ? ( ) : ( <>
{user?.name || 'Guest'}
{viewerCount} {viewerCount === 1 ? 'viewer' : 'viewers'} online
{shareStatus ? {shareStatus} : null}
{activeTab === 'scoreboard' && } {activeTab === 'plays' && } {activeTab === 'history' && } {activeTab === 'standings' && } {activeTab === 'charts' && }
)}
); } function ReactionBar({ targetId }) { const { counts, userReactions, toggle, loading } = useReactions(targetId); const emojis = ['👍', '❤️', '🎉', '🔥', '👀']; if (loading) return null; return (
{emojis.map((emoji) => { const count = counts?.[emoji] || 0; const isActive = Boolean(userReactions?.includes?.(emoji)); return ( ); })}
); } const PLAY_REACTIONS = { ReactionBar, }; function GameHistoryList() { const { items, loading } = useCollection('game_history'); if (loading) { return (

Game History

Loading historical results…

); } const sortedItems = [...(items || [])].sort((a, b) => new Date(b?.timestamp || 0) - new Date(a?.timestamp || 0)); return (

Game History

{sortedItems.length === 0 ? (

No historical game records yet.

) : (
{sortedItems.map((entry) => (
{entry?.teamName || 'Team'} vs {entry?.opponentName || 'Opponent'} · {entry?.result || 'recorded'}
{Number(entry?.points || 0)} pts {entry?.timestamp ? new Date(entry.timestamp).toLocaleString() : 'Unknown time'}
))}
)}
); } function SeasonStandings() { const { items, loading } = useCollection('season_standings'); if (loading) { return (

Season Standings

Loading standings…

); } const sortedItems = [...(items || [])].sort((a, b) => { const pointsDiff = Number(b?.standingsPoints || 0) - Number(a?.standingsPoints || 0); if (pointsDiff !== 0) return pointsDiff; return Number(b?.pointDiff || 0) - Number(a?.pointDiff || 0); }); return (

Season Standings

{sortedItems.length === 0 ? (

No standings recorded yet.

) : (
Team W L T PF PA Diff Pts
{sortedItems.map((team) => (
{team?.teamName || 'Team'} {Number(team?.wins || 0)} {Number(team?.losses || 0)} {Number(team?.ties || 0)} {Number(team?.pointsFor || 0)} {Number(team?.pointsAgainst || 0)} {Number(team?.pointDiff || 0)} {Number(team?.standingsPoints || 0)}
))}
)}
); } function SeasonPerformanceCharts() { const { items: standingsItems, loading: standingsLoading } = useCollection('season_standings'); const { items: historyItems, loading: historyLoading } = useCollection('game_history'); if (standingsLoading || historyLoading) { return (

Season Performance

Loading charts…

); } const teamsMap = new Map(); (standingsItems || []).forEach((team) => { const key = team?.teamId || team?.id || team?.teamName; if (!key) return; teamsMap.set(key, { teamId: team?.teamId || key, teamName: team?.teamName || 'Team', seasonId: team?.seasonId || 'current-season', wins: Number(team?.wins || 0), losses: Number(team?.losses || 0), ties: Number(team?.ties || 0), pointsFor: Number(team?.pointsFor || 0), played: Number(team?.played || 0), }); }); (historyItems || []).forEach((entry) => { const key = entry?.teamId || entry?.teamName; if (!key) return; const existing = teamsMap.get(key) || { teamId: key, teamName: entry?.teamName || 'Team', seasonId: entry?.seasonId || 'current-season', wins: 0, losses: 0, ties: 0, pointsFor: 0, played: 0, }; existing.teamName = existing.teamName || entry?.teamName || 'Team'; existing.pointsFor += Number(entry?.points || 0); existing.played += 1; const result = String(entry?.result || '').toLowerCase(); if (result === 'win') existing.wins += 1; else if (result === 'loss') existing.losses += 1; else if (result === 'tie' || result === 'draw') existing.ties += 1; teamsMap.set(key, existing); }); const chartTeams = Array.from(teamsMap.values()).filter((team) => team?.teamName); const sortedByPoints = [...chartTeams].sort((a, b) => { const aPpg = a.played > 0 ? a.pointsFor / a.played : 0; const bPpg = b.played > 0 ? b.pointsFor / b.played : 0; return bPpg - aPpg; }); const sortedByRecord = [...chartTeams].sort((a, b) => { const aTotal = a.wins + a.losses + a.ties; const bTotal = b.wins + b.losses + b.ties; const aWinRate = aTotal > 0 ? (a.wins / aTotal) : 0; const bWinRate = bTotal > 0 ? (b.wins / bTotal) : 0; return bWinRate - aWinRate; }); const ppgLabels = sortedByPoints.map((team) => team.teamName); const ppgData = sortedByPoints.map((team) => Number(((team.played > 0 ? team.pointsFor / team.played : 0).toFixed(2)))); const winLabels = sortedByRecord.map((team) => team.teamName); const winsData = sortedByRecord.map((team) => Number(team.wins || 0)); const lossesData = sortedByRecord.map((team) => Number(team.losses || 0)); return (

Season Performance

Points per game and win/loss record
{chartTeams.length === 0 ? (

No season data available yet.

) : (

Points per Game

Win / Loss Record

)}
); } function SeasonBarChart({ labels = [], values = [], valueSuffix = '', barClassName = '', emptyLabel = 'No data available' }) { const maxValue = Math.max(0, ...values.map((value) => Number(value || 0))); if (!labels.length || !values.length) { return

{emptyLabel}

; } return (
{labels.map((label, index) => { const value = Number(values[index] || 0); const height = maxValue > 0 ? Math.max(8, (value / maxValue) * 100) : 8; return (
{value.toFixed(2)}{valueSuffix} {label}
); })}
); } function SeasonStackedRecordChart({ labels = [], wins = [], losses = [], emptyLabel = 'No data available' }) { const maxTotal = Math.max(0, ...labels.map((_, index) => Number(wins[index] || 0) + Number(losses[index] || 0))); if (!labels.length) { return

{emptyLabel}

; } return (
{labels.map((label, index) => { const winValue = Number(wins[index] || 0); const lossValue = Number(losses[index] || 0); const total = winValue + lossValue; const winWidth = maxTotal > 0 ? (winValue / maxTotal) * 100 : 0; const lossWidth = maxTotal > 0 ? (lossValue / maxTotal) * 100 : 0; return (
{label} {winValue}-{lossValue}{total > 0 ? ` (${total})` : ''}
Wins {winValue} Losses {lossValue}
); })}
); } ReactDOM.createRoot(document.getElementById('root')).render();