Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions apps/frontend/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"reports": {
"page-title": "Reports",
"menu": {
"field-status": "Field Status",
"team-list": "Team List",
"pit-map": "Pit Map",
"awards-list": "Awards list",
Expand All @@ -174,6 +175,67 @@
"field-schedule": "Field Schedule",
"judging-schedule": "Judging Schedule"
},
"field-status": {
"countdown": {
"no-match": "No match loaded",
"late": "Late!",
"tables-ready": "{ready} of {total} tables ready"
},
"upcoming-matches": {
"title": "Upcoming Matches",
"no-matches": "No upcoming matches",
"time": "Time",
"next": "Next",
"in-time": "in {time}",
"team-number": "Team {number}",
"match-number": "Match",
"scheduled-time": "Scheduled Time",
"teams": "Teams"
},
"active-match": {
"title": "Active Match",
"no-match": "No active match",
"match-title": "Match {slug}",
"started-at": "Started",
"deviation": "Deviation",
"participants": "Participants",
"team-label": "Team",
"status": {
"in-progress": "Running",
"completed": "Completed"
}
},
"next-match": {
"title": "Next Match",
"no-match": "No match loaded",
"match-title": "Next Match: {slug}",
"tables-ready": "Tables Ready",
"team-number": "Team {number}",
"judging-conflict": "Judging: {room}",
"status": {
"ready": "Ready",
"present": "Present",
"queued": "Queued",
"not-present": "Not Present"
}
},
"judging-integration": {
"title": "Judging Integration",
"active-sessions": "Active Judging Sessions",
"team-info": "Team {number} - {name}",
"conflicts": "Time Conflicts",
"team-number": "Team {number}",
"conflict-details": "In judging: {room} | Called to match {match}",
"no-conflicts": "No time conflicts"
},
"queue-overview": {
"title": "Queue Status",
"no-matches": "No matches in queue right now",
"match-label": "Match",
"in-queue": "In queue:",
"team-label": "Team"
}
},
"awards-list": {
"page-title": "Awards list",
"description": "A list of all awards in this division, including their order of distribution and their settings.",
Expand Down
62 changes: 62 additions & 0 deletions apps/frontend/locale/he.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@
"reports": {
"page-title": "דוחות",
"menu": {
"field-status": "מצב הזירה",
"team-list": "רשימת קבוצות",
"pit-map": "מפת פיטים",
"awards-list": "רשימת פרסים",
Expand All @@ -174,6 +175,67 @@
"judging-schedule": "לו״ז שיפוט",
"field-timer": "שעון זמן מגרש"
},
"field-status": {
"countdown": {
"no-match": "אין מקצה טעון",
"late": "מאחר!",
"tables-ready": "{ready} מתוך {total} שולחנות מוכנים"
},
"upcoming-matches": {
"title": "מקצים קרובים",
"no-matches": "אין מקצים קרובים",
"time": "זמן",
"next": "הבא",
"in-time": "בעוד {time}",
"team-number": "קבוצה {number}",
"match-number": "מקצה",
"scheduled-time": "זמן מתוכנן",
"teams": "קבוצות"
},
"active-match": {
"title": "מקצה רץ",
"no-match": "אין מקצה פעיל כרגע",
"match-title": "מקצה {slug}",
"started-at": "התחיל",
"deviation": "סטייה",
"participants": "משתתפים",
"team-label": "קבוצה",
"status": {
"in-progress": "רץ",
"completed": "הושלם"
}
},
"next-match": {
"title": "המקצה הבא",
"no-match": "אין מקצה טעון",
"match-title": "המקצה הבא: {slug}",
"tables-ready": "שולחנות מוכנים",
"team-number": "קבוצה {number}",
"judging-conflict": "שיפוט: {room}",
"status": {
"ready": "מוכן",
"present": "נוכח",
"queued": "בקיו",
"not-present": "לא נוכח"
}
},
"judging-integration": {
"title": "אינטגרציה עם שיפוט",
"active-sessions": "מפגשי שיפוט פעילים",
"team-info": "קבוצה {number} - {name}",
"conflicts": "התנגשויות זמן",
"team-number": "קבוצה {number}",
"conflict-details": "בשיפוט: {room} | נקראה למקצה {match}",
"no-conflicts": "אין התנגשויות זמן"
},
"queue-overview": {
"title": "מצב הקיו",
"no-matches": "אין מקצים בקיו כרגע",
"match-label": "מקצה",
"in-queue": "בקיו:",
"team-label": "קבוצה"
}
},
"awards-list": {
"page-title": "רשימת פרסים",
"description": "רשימת כל הפרסים בחלוקה זו, כולל סדר ההענקה וההגדרות שלהם.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client';

import { useTranslations } from 'next-intl';
import dayjs from 'dayjs';
import { Paper, Stack, Typography, Chip, Box } from '@mui/material';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled';
import { useMatchTranslations } from '@lems/localization';
import { useTime } from '../../../../../../../../lib/time/hooks';

interface Participant {
id: string;
team?: {
id: string;
number: number;
name: string;
} | null;
table: {
id: string;
name: string;
};
queued: boolean;
present: boolean;
ready: boolean;
}

interface Match {
id: string;
slug: string;
stage: string;
round: number;
number: number;
scheduledTime: string;
status: string;
startTime?: string | null;
startDelta?: number | null;
participants: Participant[];
}

interface ActiveMatchPanelProps {
match: Match | null;
matchLength?: number;
}

/**
* Display panel for currently active match
* Shows match info, participants, and progress
*/
export function ActiveMatchPanel({ match }: ActiveMatchPanelProps) {
const t = useTranslations('pages.reports.field-status');
const { getStage } = useMatchTranslations();
const currentTime = useTime({ interval: 1000 });

if (!match) {
return (
<Paper sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack spacing={2} sx={{ flex: 1 }}>
<Typography variant="h5" fontWeight={600}>
{t('active-match.title')}
</Typography>
<Typography color="text.secondary">{t('active-match.no-match')}</Typography>
</Stack>
</Paper>
);
}

const getStatusColor = () => {
if (match.status === 'in-progress') return 'success';
if (match.status === 'completed') return 'primary';
return 'default';
};

const getStatusText = () => {
if (match.status === 'in-progress') return t('active-match.status.in-progress');
if (match.status === 'completed') return t('active-match.status.completed');
return match.status;
};

const formatDelay = (delta: number) => {
const minutes = Math.floor(Math.abs(delta) / 60);
const seconds = Math.abs(delta) % 60;
const sign = delta > 0 ? '+' : '-';
return `${sign}${minutes}:${String(seconds).padStart(2, '0')}`;
};

return (
<Paper sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
<Stack spacing={2} sx={{ flex: 1 }}>
<Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
<Typography variant="h5" fontWeight={700} sx={{ fontSize: '1.35rem' }}>
{getStage(match.stage)} #{match.number}
</Typography>
<Chip
label={getStatusText()}
color={getStatusColor()}
icon={match.status === 'in-progress' ? <PlayCircleFilledIcon /> : <CheckCircleIcon />}
/>
</Stack>

{match.startTime && (
<Stack spacing={0.5}>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontSize: '1.05rem', fontWeight: 700 }}
>
{t('active-match.started-at')}:{' '}
{currentTime
.set('hour', dayjs(match.startTime).hour())
.set('minute', dayjs(match.startTime).minute())
.set('second', dayjs(match.startTime).second())
.format('HH:mm:ss')}
</Typography>
{match.startDelta !== null && match.startDelta !== undefined && (
<Typography
variant="body2"
color={Math.abs(match.startDelta) > 120 ? 'error' : 'text.secondary'}
sx={{ fontSize: '1.05rem', fontWeight: 700 }}
>
{t('active-match.deviation')}: {formatDelay(match.startDelta)}
</Typography>
)}
</Stack>
)}

<Box>
<Typography
variant="subtitle2"
color="text.secondary"
gutterBottom
sx={{
fontSize: '0.95rem',
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: 0.5
}}
>
{t('active-match.participants')}:
</Typography>
<Stack spacing={1}>
{match.participants
.filter(p => p.team)
.map(participant => (
<Stack
key={participant.id}
direction="row"
spacing={0.5}
alignItems="center"
sx={{
py: 1,
px: 2,
bgcolor: 'background.default',
borderRadius: 1
}}
>
<Typography
variant="body2"
fontWeight={700}
sx={{ minWidth: 80, fontSize: '1.05rem' }}
>
{participant.table.name}:
</Typography>
<Typography variant="body2" sx={{ fontSize: '1.05rem', fontWeight: 700 }}>
{t('active-match.team-label')} {participant.team?.number} -{' '}
{participant.team?.name}
</Typography>
{participant.ready && <CheckCircleIcon fontSize="small" color="success" />}
</Stack>
))}
</Stack>
</Box>
</Stack>
</Paper>
);
}
Loading