diff --git a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/current-season.tsx b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/current-season.tsx
index 63b9da993..98787b5a4 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/current-season.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/current-season.tsx
@@ -19,20 +19,25 @@ export default async function CurrentSeasonWidget() {
+ alignItems: 'center',
+ textAlign: 'center'
+ }}
+ >
+ }}
+ >
{t('error')}
-
+
{t('error-description')}
@@ -62,11 +67,12 @@ export default async function CurrentSeasonWidget() {
+ width: '100%'
+ }}
+ >
+ }}
+ >
{t('current-season')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/number-widget.tsx b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/number-widget.tsx
index a3e3db3db..a15026c89 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/number-widget.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/number-widget.tsx
@@ -13,11 +13,12 @@ export default function NumberWidget({ value, description, icon }: NumberWidgetP
+ width: '100%'
+ }}
+ >
{icon && (
+ }}
+ >
{description}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/upcoming-events/event-list-item.tsx b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/upcoming-events/event-list-item.tsx
index 82a47fb1f..51b974998 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/components/widgets/upcoming-events/event-list-item.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/components/widgets/upcoming-events/event-list-item.tsx
@@ -28,11 +28,12 @@ export default function EventListItem({ event }: EventListItemProps) {
>
+ }}
+ >
+ }}
+ >
{event.location}
+ }}
+ >
+ }}
+ >
{t('no-events')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/dev/graphql/page.tsx b/apps/admin/src/app/[locale]/(dashboard)/dev/graphql/page.tsx
index 40a195bd2..10e973ce8 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/dev/graphql/page.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/dev/graphql/page.tsx
@@ -38,9 +38,10 @@ export default function GraphQLSchemaPage() {
+ }}
+ >
Explore and interact with the GraphQL schema for the LEMS application.
diff --git a/apps/admin/src/app/[locale]/(dashboard)/error.tsx b/apps/admin/src/app/[locale]/(dashboard)/error.tsx
index 30d3f091f..483275bdd 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/error.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/error.tsx
@@ -16,9 +16,12 @@ export default function Error({
return (
-
+
Something went wrong
{error.message || 'Unexpected error'}
@@ -107,16 +116,20 @@ export const AwardsValidation = () => {
+ }}
+ >
{t('maximum.title')}
-
+
{maximumAwards} {t('awards')} ({maximumPercentage}%)
@@ -137,10 +150,11 @@ export const AwardsValidation = () => {
+ }}
+ >
{t('maximum.description')}
@@ -159,19 +173,28 @@ export const AwardsValidation = () => {
{t('rules.title')}
-
+
• {t('rules.teams-count')}
-
+
• {t('rules.minimum-requirement')}
-
+
• {t('rules.maximum-requirement')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/division-selector.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/division-selector.tsx
index 4a6d40156..d3c1249c4 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/division-selector.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/division-selector.tsx
@@ -30,9 +30,14 @@ export const DivisionSelector: React.FC = ({ divisions })
{t('label')}
-
+
{divisions.map(division => {
const isSelected = division.id === selectedDivisionId;
const divisionColor = division.color || '#666';
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/event-page-title.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/event-page-title.tsx
index 56c19a944..0a89d1328 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/event-page-title.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/components/event-page-title.tsx
@@ -27,12 +27,17 @@ export function EventPageTitle({ title, children }: EventPageTitleProps) {
-
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+
= ({
/>
-
+
setFieldValue('color', color)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/divisions/components/divisions-table.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/divisions/components/divisions-table.tsx
index 0f867f20d..8deb1325f 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/divisions/components/divisions-table.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/divisions/components/divisions-table.tsx
@@ -143,10 +143,11 @@ export const DivisionsTable: React.FC = ({ divisions, onEdi
{editingDivision === division.id ? (
+ }}
+ >
setEditForm(prev => ({ ...prev, color }))}
@@ -175,9 +176,10 @@ export const DivisionsTable: React.FC = ({ divisions, onEdi
+ }}
+ >
= ({ divisions, onEdi
{editingDivision === division.id ? (
-
+
handleEditSave(division)}
color="primary"
@@ -211,9 +217,13 @@ export const DivisionsTable: React.FC = ({ divisions, onEdi
) : (
-
+
handleEditStart(division)}
color="primary"
@@ -232,9 +242,12 @@ export const DivisionsTable: React.FC = ({ divisions, onEdi
{!hasMultipleDivisions && (
-
+
{t('list.alerts.not-enough-divisions')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/division-color-editor.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/division-color-editor.tsx
index 80bdaa8ee..e8d89a650 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/division-color-editor.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/division-color-editor.tsx
@@ -51,24 +51,29 @@ export const DivisionColorEditor: React.FC = ({ divisi
return (
+ }}
+ >
-
+
{t('color')}
{isEditingColor ? (
+ }}
+ >
setEditColor(color)} defaultOpen>
= ({ divisi
sx={{
fontWeight: 500,
fontFamily: 'monospace'
- }}>
+ }}
+ >
{hsvaToHex(editColor)}
@@ -106,9 +112,10 @@ export const DivisionColorEditor: React.FC = ({ divisi
+ }}
+ >
= ({ divisi
sx={{
fontWeight: 500,
fontFamily: 'monospace'
- }}>
+ }}
+ >
{division.color}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/edit-event-card.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/edit-event-card.tsx
index ab694987b..b20690f6a 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/edit-event-card.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/edit-event-card.tsx
@@ -48,9 +48,10 @@ const EditEventCard: React.FC = ({ icon, title, href, sx })
+ }}
+ >
{t(title)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/editable-event-title.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/editable-event-title.tsx
index a085f7d3e..c28d133fb 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/editable-event-title.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/edit/components/editable-event-title.tsx
@@ -78,9 +78,13 @@ export const EditableEventTitle: React.FC = () => {
if (isEditing) {
return (
-
+
{
}
}}
/>
-
+
{
direction="row"
spacing={1}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
{
return (
-
+
{singleDivision && (
)}
+ }}
+ >
-
+
{t('date')}
-
+
{dayjs(event.startDate).format('MMM D, YYYY')}
@@ -63,20 +74,27 @@ export const EventInformation = () => {
+ }}
+ >
-
+
{t('location')}
-
+
{event.location || 'Not specified'}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/add-integration-card.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/add-integration-card.tsx
index cd261ff11..6f86ccc17 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/add-integration-card.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/add-integration-card.tsx
@@ -43,10 +43,11 @@ export const AddIntegrationCard: React.FC = ({ onClick
+ }}
+ >
= ({
/>
-
+
{getIntegrationName(integration.type)}
{getIntegrationDescription(integration.type) && (
-
+
{getIntegrationDescription(integration.type)}
)}
@@ -209,9 +215,12 @@ export const AddIntegrationDialog: React.FC = ({
) : (
-
+
{searchQuery ? t('add-dialog.no-results') : t('add-dialog.no-available-integrations')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/integration-card.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/integration-card.tsx
index 37af28739..3763acd1d 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/integration-card.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/integration-card.tsx
@@ -58,10 +58,11 @@ export const IntegrationCard: React.FC = ({
+ }}
+ >
= ({
flex: 1
}}
>
-
+
{t('detail-panel.no-selection')}
@@ -131,9 +134,12 @@ export const IntegrationDetailPanel: React.FC = ({
borderColor: 'divider'
}}
>
-
+
{t('detail-panel.settings-placeholder')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid/components/sendgrid-settings.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid/components/sendgrid-settings.tsx
index e83517cb9..d95d8dfac 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid/components/sendgrid-settings.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/integrations/components/settings/sendgrid/components/sendgrid-settings.tsx
@@ -111,9 +111,13 @@ const SendGridSettingsContent: React.FC = ({
bgcolor: 'action.hover'
}}
>
-
+
{t('no-contacts-uploaded')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-exists.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-exists.tsx
index 0f83e5be4..11157a7a0 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-exists.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-exists.tsx
@@ -57,9 +57,12 @@ export const ScheduleExists: React.FC = ({ division }) => {
return (
<>
-
+
}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-manager.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-manager.tsx
index 791a8d7cf..e2b67f3db 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-manager.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-manager.tsx
@@ -27,9 +27,12 @@ const ScheduleManagerContent: React.FC = ({ division }) =>
if (teamsCount === 0 || roomsCount === 0 || tablesCount === 0) {
return (
-
+
} sx={{ py: 0.5 }}>
{t('alerts.missing-details')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-settings.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-settings.tsx
index 8a8a8f49d..a97ce4ecd 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-settings.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/schedule-settings.tsx
@@ -77,9 +77,13 @@ export const ScheduleSettings: React.FC = () => {
}}
>
-
+
{t('title')}
@@ -87,33 +91,50 @@ export const ScheduleSettings: React.FC = () => {
+ }}
+ >
{t('information.title')}
-
-
+
+
{t('information.teams')}: {teamsCount}
-
+
{t('information.rooms')}: {roomsCount}
-
+
{t('information.tables')}: {tablesCount}
@@ -123,20 +144,32 @@ export const ScheduleSettings: React.FC = () => {
-
-
+
+
{t('information.total-matches')}: {totalMatches}
-
+
{t('information.total-sessions')}: {totalSessions}
@@ -146,28 +179,44 @@ export const ScheduleSettings: React.FC = () => {
-
-
+
+
{t('information.practice-rounds')}: {practiceRounds}
-
+
{t('information.ranking-rounds')}: {rankingRounds}
-
+
{t('information.matches-per-round')}: {matchesPerRound}
@@ -177,28 +226,44 @@ export const ScheduleSettings: React.FC = () => {
-
-
+
+
{t('information.judging-start')}: {judgingStart.format('HH:mm')}
-
+
{t('information.field-start')}: {fieldStart.format('HH:mm')}
-
+
{t('settings.timezone')}: {timezone}
@@ -213,15 +278,20 @@ export const ScheduleSettings: React.FC = () => {
+ }}
+ >
{t('settings.title')}
-
+
= ({
+ color: 'text.secondary',
+ textAlign: 'center'
+ }}
+ >
{t('select-team-to-continue')}
@@ -101,17 +102,24 @@ export const JudgingSessionSelector: React.FC = ({
-
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+
{room.name}
-
+
{isEmpty
? t('empty')
: isCurrentTeam
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-schedule-view.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-schedule-view.tsx
index 37775aa02..26a8927c9 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-schedule-view.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-schedule-view.tsx
@@ -53,9 +53,12 @@ export const TeamScheduleView: React.FC = ({ teamSchedule
{teamSchedule.matches.length > 0 && (
<>
-
+
{t('matches')}
{teamSchedule.matches.map(match => (
@@ -64,9 +67,10 @@ export const TeamScheduleView: React.FC = ({ teamSchedule
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
{t('match', {
round: match.round,
@@ -74,9 +78,12 @@ export const TeamScheduleView: React.FC = ({ teamSchedule
stage: getStage(match.stage)
})}
-
+
{dayjs(match.scheduledTime).format('HH:mm')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-selector.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-selector.tsx
index 55c1c4b4a..8d6eae20f 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-selector.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-selector.tsx
@@ -37,9 +37,10 @@ export const TeamSelector: React.FC = ({
variant="body2"
gutterBottom
sx={{
- fontSize: "small",
- color: "text.secondary"
- }}>
+ fontSize: 'small',
+ color: 'text.secondary'
+ }}
+ >
{t('description')}
= ({
+ }}
+ >
{searchQuery ? t('no-teams-found') : t('no-teams')}
) : (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-swapper.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-swapper.tsx
index 0e840d0b5..8e7e50261 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-swapper.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/schedule/components/team-swapper/team-swapper.tsx
@@ -135,9 +135,10 @@ export const TeamSwapper: React.FC = ({ division }) => {
+ }}
+ >
= ({ division }) => {
sx={{
p: 3,
flex: 1,
- display: "flex",
- flexDirection: "column",
- overflow: "hidden"
- }}>
+ display: 'flex',
+ flexDirection: 'column',
+ overflow: 'hidden'
+ }}
+ >
= ({
);
-};
\ No newline at end of file
+};
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/danger-zone-section.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/danger-zone-section.tsx
index e1fe18ff3..849688f94 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/danger-zone-section.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/danger-zone-section.tsx
@@ -1,48 +1,40 @@
-"use client";
+'use client';
-import React from "react";
-import { useTranslations } from "next-intl";
-import {
- Box,
- Card,
- CardContent,
- Typography,
- Button,
- Stack,
- useTheme,
-} from "@mui/material";
+import React from 'react';
+import { useTranslations } from 'next-intl';
+import { Box, Card, CardContent, Typography, Button, Stack, useTheme } from '@mui/material';
export const DangerZoneSection = () => {
- const t = useTranslations("pages.events.settings.danger-zone");
+ const t = useTranslations('pages.events.settings.danger-zone');
const theme = useTheme();
return (
- {t("title")}
+ {t('title')}
-
-
- {t("delete-event")}
+
+
+ {t('delete-event')}
-
- {t("delete-event-description")}
-
-
- {t("delete-disabled")}
+ {t('delete-event-description')}
+
+ {t('delete-disabled')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/download-results-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/download-results-dialog.tsx
index e84859dd8..d517cfbb5 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/download-results-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/download-results-dialog.tsx
@@ -52,16 +52,22 @@ export const DownloadResultsDialog: React.FC = ({
{t('event-name', { eventName })}
-
+
{t('info')}
{progress !== null && (
-
+
{t('generating')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-actions-section.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-actions-section.tsx
index 78d1421ed..e6f71c6a4 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-actions-section.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-actions-section.tsx
@@ -81,8 +81,7 @@ export const EventActionsSection: React.FC = ({
setAlert({ type: 'error', message: t('messages.publish-error') });
}
} catch (error) {
- const errorMsg =
- error instanceof Error ? error.message : t('messages.publish-error');
+ const errorMsg = error instanceof Error ? error.message : t('messages.publish-error');
setAlert({ type: 'error', message: errorMsg });
}
};
@@ -127,9 +126,13 @@ export const EventActionsSection: React.FC = ({
-
+
= ({
{t('event-actions.complete-event-description')}
{settings.completed && (
-
+
{t('event-actions.already-completed')}
)}
@@ -157,9 +163,13 @@ export const EventActionsSection: React.FC = ({
-
+
= ({
{t('event-actions.publish-event-description')}
{settings.published && (
-
+
{t('event-actions.already-published')}
)}
@@ -187,9 +200,13 @@ export const EventActionsSection: React.FC = ({
-
+
= ({
{t('event-actions.download-results-description')}
{!settings.published && (
-
+
{t('event-actions.not-published')}
)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-settings-section.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-settings-section.tsx
index 407640ac5..64595c838 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-settings-section.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/event-settings-section.tsx
@@ -131,9 +131,10 @@ export const EventSettingsSection: React.FC = ({
+ }}
+ >
{t('event-settings.visible-description')}
= ({
+ }}
+ >
{t('event-settings.official-description')}
= ({
+ }}
+ >
{t('event-settings.open-rubrics-during-session-description')}
@@ -180,13 +183,18 @@ export const EventSettingsSection: React.FC = ({
size={4}
spacing={3}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mt: 1,
mb: 4
- }}>
-
+ }}
+ >
+
{t('event-settings.advancement-percent')}
{totalTeams > 0 && (
@@ -194,9 +202,12 @@ export const EventSettingsSection: React.FC = ({
)}
-
+
{t('event-settings.advancement-percent-description')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/publish-event-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/publish-event-dialog.tsx
index acfee72e2..7b5daf903 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/publish-event-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/components/publish-event-dialog.tsx
@@ -59,9 +59,12 @@ export const PublishEventDialog: React.FC = ({
{t('event-name', { eventName })}
-
+
{t('info')}
{progress !== null && (
@@ -75,9 +78,12 @@ export const PublishEventDialog: React.FC = ({
) : (
-
+
{t('publishing-progress')}
@@ -103,4 +109,4 @@ export const PublishEventDialog: React.FC = ({
);
-};
\ No newline at end of file
+};
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/page.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/page.tsx
index 463ae3ebc..a79f88478 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/page.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/settings/page.tsx
@@ -1,39 +1,39 @@
-"use client";
+'use client';
-import React, { useState } from "react";
-import useSWR from "swr";
-import { useTranslations } from "next-intl";
-import { Box, Stack, Alert, CircularProgress } from "@mui/material";
-import { EventSettings } from "@lems/types/api/admin";
-import { EventPageTitle } from "../components/event-page-title";
-import { useEvent } from "../components/event-context";
-import { EventSettingsSection } from "./components/event-settings-section";
-import { EventActionsSection } from "./components/event-actions-section";
-import { DangerZoneSection } from "./components/danger-zone-section";
+import React, { useState } from 'react';
+import useSWR from 'swr';
+import { useTranslations } from 'next-intl';
+import { Box, Stack, Alert, CircularProgress } from '@mui/material';
+import { EventSettings } from '@lems/types/api/admin';
+import { EventPageTitle } from '../components/event-page-title';
+import { useEvent } from '../components/event-context';
+import { EventSettingsSection } from './components/event-settings-section';
+import { EventActionsSection } from './components/event-actions-section';
+import { DangerZoneSection } from './components/danger-zone-section';
const SettingsPage: React.FC = () => {
- const t = useTranslations("pages.events.settings");
+ const t = useTranslations('pages.events.settings');
const event = useEvent();
const {
data: settings,
mutate: mutateSettings,
- error,
+ error
} = useSWR(`/admin/events/${event.id}/settings`, {
- suspense: false,
+ suspense: false
});
const [alert, setAlert] = useState<{
- type: "success" | "error";
+ type: 'success' | 'error';
message: string;
} | null>(null);
if (error) {
return (
-
+
- {t("messages.load-error")}
+ {t('messages.load-error')}
);
@@ -44,10 +44,10 @@ const SettingsPage: React.FC = () => {
@@ -57,14 +57,10 @@ const SettingsPage: React.FC = () => {
return (
-
+
{alert && (
- setAlert(null)}
- >
+ setAlert(null)}>
{alert.message}
)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/event-teams-split-view.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/event-teams-split-view.tsx
index 0326d25fb..e3055bc87 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/event-teams-split-view.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/event-teams-split-view.tsx
@@ -23,9 +23,12 @@ export const EventTeamsSplitView: React.FC = ({ eventId })
backgroundColor: 'grey.50'
}}
>
-
+
{t('placeholder', { eventId })}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog-content.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog-content.tsx
index b7a2797ed..88676201a 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog-content.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog-content.tsx
@@ -202,10 +202,11 @@ export const RegisterTeamsDialogContent: React.FC
+ }}
+ >
{t('no-teams-selected')}
)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog.tsx
index 75420afa5..b79d3d721 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-dialog.tsx
@@ -62,12 +62,13 @@ export const RegisterTeamsDialog = ({ open, onClose, event }: RegisterTeamsDialo
{isLoading ? (
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100%',
+ minHeight: '400px'
+ }}
+ >
) : (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-button.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-button.tsx
index 6f4adbcee..672a3619b 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-button.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-button.tsx
@@ -11,7 +11,10 @@ interface RegisterTeamsFromCSVButtonProps {
divisions: Division[];
}
-export const RegisterTeamsFromCSVButton: React.FC = ({ event, divisions }) => {
+export const RegisterTeamsFromCSVButton: React.FC = ({
+ event,
+ divisions
+}) => {
const t = useTranslations('pages.events.teams.registration-button');
const { showDialog } = useDialog();
@@ -19,16 +22,11 @@ export const RegisterTeamsFromCSVButton: React.FC 0 && divisionsWithSchedule.length === divisions.length;
const handleOpenCSVDialog = () => {
- showDialog((props) => );
+ showDialog(props => );
};
return (
-
+
{t('csv-title')}
);
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-dialog.tsx
index e746ce0be..17ea5f7b7 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/register-teams-from-csv-dialog.tsx
@@ -113,12 +113,15 @@ const RegisterForm: React.FC<{
direction="row"
spacing={2}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
mb: 1
- }}>
-
+ }}
+ >
+
@@ -128,9 +131,10 @@ const RegisterForm: React.FC<{
+ color: 'text.secondary',
+ marginBottom: '16px'
+ }}
+ >
{t('instructions.description')}
@@ -234,9 +238,12 @@ const RegisterForm: React.FC<{
placeholder={t('fields.file.placeholder')}
/>
{error && {t(`errors.${error}`)}}
-
+
+
-
+
{t('title')}
{result.registered.length > 0 && (
@@ -317,9 +330,14 @@ const SuccessView: React.FC<{
+
#{team.number} - {team.name}
@@ -376,11 +394,12 @@ export const RegisterTeamsFromCSVDialog: React.FC
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '300px'
+ }}
+ >
) : result ? (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/remove-team-button.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/remove-team-button.tsx
index 69c6e94c5..7d7097cbe 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/remove-team-button.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/remove-team-button.tsx
@@ -9,7 +9,7 @@ interface RemoveTeamButtonProps {
disabled?: boolean;
}
-export const RemoveTeamButton: React.FC = ({ team, disabled}) => {
+export const RemoveTeamButton: React.FC = ({ team, disabled }) => {
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
return (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/schedule-exists.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/schedule-exists.tsx
index f123a0ded..6f1d4b4ea 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/schedule-exists.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/teams/components/schedule-exists.tsx
@@ -69,9 +69,12 @@ export const ScheduleExists: React.FC = ({ divisions }) =>
return (
<>
-
+
{divisionsWithSchedule.map(division => (
= ({ divisions }) =>
}
>
- {(divisions.length > 1
- ? t('division-message', { divisionName: division.name })
- : t('event-message', { eventName: event.name })
- )}
+ {divisions.length > 1
+ ? t('division-message', { divisionName: division.name })
+ : t('event-message', { eventName: event.name })}
))}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/event-admins-section.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/event-admins-section.tsx
index ef6425e61..5d17b6935 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/event-admins-section.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/event-admins-section.tsx
@@ -60,18 +60,20 @@ export function EventAdminsSection() {
+ }}
+ >
{t('description')}
+ }}
+ >
{t('current-admins')}
} onClick={() => setDialogOpen(true)}>
{t('assign-admins')}
@@ -85,16 +87,20 @@ export function EventAdminsSection() {
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
{admin.firstName} {admin.lastName}
-
+
@{admin.username}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/managed-roles.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/managed-roles.tsx
index df4bd5ac8..ffd9b5537 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/managed-roles.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/managed-roles.tsx
@@ -44,17 +44,21 @@ export const ManagedRolesSection: React.FC = () => {
bgcolor: 'grey.50'
}}
>
-
+
{getRole(role)}
+ }}
+ >
{t('system-managed-roles.role-description')}
@@ -74,21 +78,26 @@ export const ManagedRolesSection: React.FC = () => {
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
-
+
{getRole(role)}
+ }}
+ >
{t('system-managed-roles.toggleable-role-description')}
@@ -107,10 +116,11 @@ export const ManagedRolesSection: React.FC = () => {
+ }}
+ >
{toggledSystemRoles.has(role)
? t('system-managed-roles.enabled')
: t('system-managed-roles.disabled')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/mandatory-roles.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/mandatory-roles.tsx
index deae33dfd..ecb7047a0 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/mandatory-roles.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/mandatory-roles.tsx
@@ -16,10 +16,11 @@ export const MandatoryRolesSection: React.FC = () => {
+ }}
+ >
{t('mandatory-roles.description')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/optional-roles.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/optional-roles.tsx
index 8d84bb423..bbbd7f3bb 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/optional-roles.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/optional-roles.tsx
@@ -17,10 +17,11 @@ export const OptionalRolesSection: React.FC = () => {
+ }}
+ >
{t('optional-roles.description')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/volunteer-users-section.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/volunteer-users-section.tsx
index 2dd9b4f29..37c66a73c 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/volunteer-users-section.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/users/components/volunteer-roles/volunteer-users-section.tsx
@@ -1,7 +1,12 @@
'use client';
import { useTranslations } from 'next-intl';
-import { Save as SaveIcon, Download as DownloadIcon, FiberNew, CheckCircle } from '@mui/icons-material';
+import {
+ Save as SaveIcon,
+ Download as DownloadIcon,
+ FiberNew,
+ CheckCircle
+} from '@mui/icons-material';
import {
Box,
Typography,
@@ -20,7 +25,8 @@ import { MandatoryRolesSection } from './mandatory-roles';
export function VolunteerUsersSection() {
const t = useTranslations('pages.events.users.sections.volunteer-users');
- const { saving, validationErrors, handleSave, loading, getEventPasswords, isNew } = useVolunteer();
+ const { saving, validationErrors, handleSave, loading, getEventPasswords, isNew } =
+ useVolunteer();
const [saveResult, setSaveResult] = useState<'success' | 'error' | null>(null);
const onSave = async () => {
@@ -41,18 +47,25 @@ export function VolunteerUsersSection() {
return (
-
+ }}
+ >
+
-
+
{t('loading')}
@@ -64,25 +77,33 @@ export function VolunteerUsersSection() {
+ }}
+ >
{t('title')}
-
+
{t('description')}
-
+
)}
-
+
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-cell.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-cell.tsx
index f3ff4aaf3..092828145 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-cell.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-cell.tsx
@@ -41,26 +41,35 @@ export const AssetCell: React.FC = ({
return (
-
+
+ }}
+ >
{index + 1}
{isEditing ? (
-
+
= ({
= ({
'&:hover': {
backgroundColor: 'action.hover'
}
- }}>
+ }}
+ >
onStartEditing()}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-manager.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-manager.tsx
index 5a56bdf24..1198c2e66 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-manager.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/asset-manager.tsx
@@ -201,9 +201,13 @@ export const AssetManager = ({ division, assetType }: Asset
{/* Add new asset */}
-
+
({ division, assetType }: Asset
+ }}
+ >
{t(`${assetType as string}.empty-state`)}
) : (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/pit-map-manager.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/pit-map-manager.tsx
index 2a4f904fe..f610bec4a 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/pit-map-manager.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/pit-map-manager.tsx
@@ -82,9 +82,13 @@ export const PitMapManager: React.FC = ({ division, onDivisi
{t('title')}
-
+
= ({ division, onDivisi
{!division.pitMapUrl && (
-
+
{t('no-map')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/schedule-exists.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/schedule-exists.tsx
index ca9c28d22..f291483b1 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/schedule-exists.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/[slug]/venue/components/schedule-exists.tsx
@@ -86,7 +86,7 @@ export const ScheduleExists: React.FC = ({ division }) => {
}
>
- {t('message', { divisionName: division.name }) }
+ {t('message', { divisionName: division.name })}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/components/current-season.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/components/current-season.tsx
index a2d606bf0..b4d5a2f92 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/components/current-season.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/components/current-season.tsx
@@ -12,9 +12,11 @@ interface CurrentSeasonProps {
export const CurrentSeason: React.FC = ({ season }) => {
return (
<>
-
+
= ({
-
+
{location}
@@ -85,18 +88,24 @@ export const EventCard: React.FC = ({
-
+
{dayjs(date).format('DD/MM/YYYY')}
-
+
{teamCount} {t('teams')}
@@ -107,9 +116,10 @@ export const EventCard: React.FC = ({
+ }}
+ >
{t('divisions')}:
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/components/event-grid.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/components/event-grid.tsx
index 87343dfeb..e8c302c61 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/components/event-grid.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/components/event-grid.tsx
@@ -65,7 +65,12 @@ export const EventGrid: React.FC = ({
{t('empty-state.no-events-description')}
- }>
+ }
+ >
{t('empty-state.create-new-event')}
>
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/components/missing-info/missing-info-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/components/missing-info/missing-info-dialog.tsx
index e022cac49..6fc463745 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/components/missing-info/missing-info-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/components/missing-info/missing-info-dialog.tsx
@@ -88,9 +88,12 @@ export const MissingInfoDialog: React.FC = ({
{t('dialog-title')}
{!hasDetailedData ? (
-
+
{tCard('missing-details')}
) : (
@@ -129,9 +132,10 @@ export const MissingInfoDialog: React.FC = ({
+ }}
+ >
{t('division-fully-configured')}
)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/components/previous-season.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/components/previous-season.tsx
index dd8e8c6a0..07d31095d 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/components/previous-season.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/components/previous-season.tsx
@@ -40,9 +40,10 @@ export const PreviousSeason: React.FC = ({ season }) => {
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
= ({
-
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+
= ({
{seasonName}
{numberOfEvents > 0 && (
-
+
{numberOfEvents} {numberOfEvents === 1 ? 'event' : 'events'}
)}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/create/components/create-event-layout.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/create/components/create-event-layout.tsx
index 19b13713b..a1a17981d 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/create/components/create-event-layout.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/create/components/create-event-layout.tsx
@@ -192,10 +192,11 @@ export const CreateEventLayout = () => {
<>
+ }}
+ >
{t('sections.event-details')}
@@ -286,9 +287,11 @@ export const CreateEventLayout = () => {
{isMultipleDivisions && (
-
+
{t('sections.divisions')}
@@ -308,9 +311,13 @@ export const CreateEventLayout = () => {
)}
-
+
}
onClick={addDivision}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/events/create/components/division-item.tsx b/apps/admin/src/app/[locale]/(dashboard)/events/create/components/division-item.tsx
index f6fcd8466..a65162f68 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/events/create/components/division-item.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/events/create/components/division-item.tsx
@@ -25,11 +25,12 @@ export const DivisionItem: React.FC = ({
+ }}
+ >
updateDivisionField(index, 'color', hsvaColor)}
@@ -50,9 +51,12 @@ export const DivisionItem: React.FC = ({
/>
-
+
{t('title', { number: index + 1 })}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/layout.tsx b/apps/admin/src/app/[locale]/(dashboard)/layout.tsx
index 136e08056..44d36cc3d 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/layout.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/layout.tsx
@@ -96,10 +96,11 @@ const AppBar: React.FC = ({ width, permissions, user }) => {
+ width: '100%',
+ height: '80%',
+ position: 'relative'
+ }}
+ >
diff --git a/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-card.tsx b/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-card.tsx
index d54def8aa..e453614a4 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-card.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-card.tsx
@@ -23,12 +23,13 @@ export const CreateSeasonCard = () => {
sx={{
minHeight: 250,
p: 2,
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- flexDirection: "column",
- color: "text.primary"
- }}>
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexDirection: 'column',
+ color: 'text.primary'
+ }}
+ >
{t('create-new-season')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-dialog.tsx
index ecafb0135..c246c176f 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/seasons/components/create-season-dialog.tsx
@@ -193,9 +193,13 @@ const CreationForm: React.FC = ({ onSuccess }) => {
{status && {t(`errors.${status}`)}}
-
+
= ({ season }) => {
variant="outlined"
sx={{
pt: 3,
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- flexDirection: "column"
- }}>
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ flexDirection: 'column'
+ }}
+ >
= ({ seasons: initialSeason
return (
- {seasons?.map(season => )}
+ {seasons?.map(season => (
+
+ ))}
);
};
diff --git a/apps/admin/src/app/[locale]/(dashboard)/teams/components/import-team-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/teams/components/import-team-dialog.tsx
index 9022895c8..3167d1c60 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/teams/components/import-team-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/teams/components/import-team-dialog.tsx
@@ -84,12 +84,15 @@ const ImportForm: React.FC<{ onSuccess: (result: ImportResult) => void }> = ({ o
direction="row"
spacing={2}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
mb: 1
- }}>
-
+ }}
+ >
+
@@ -99,9 +102,10 @@ const ImportForm: React.FC<{ onSuccess: (result: ImportResult) => void }> = ({ o
+ color: 'text.secondary',
+ marginBottom: '16px'
+ }}
+ >
{t('instructions.description')}
@@ -163,9 +167,12 @@ const ImportForm: React.FC<{ onSuccess: (result: ImportResult) => void }> = ({ o
placeholder={t('fields.file.placeholder')}
/>
{error && {t(`errors.${error}`)}}
-
+
void }> = ({
const t = useTranslations('pages.teams.import-dialog.success');
return (
-
+
-
+
{t('title')}
{result.created.length > 0 && (
diff --git a/apps/admin/src/app/[locale]/(dashboard)/teams/components/logo-upload-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/teams/components/logo-upload-dialog.tsx
index 7e50c7baf..f46766168 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/teams/components/logo-upload-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/teams/components/logo-upload-dialog.tsx
@@ -51,13 +51,10 @@ export const LogoUploadDialog: React.FC = ({ team, open,
formData.append('city', team.city);
formData.append('logo', selectedFile);
- const response = await apiFetch(
- `/admin/teams/${team.id}`,
- {
- method: 'PUT',
- body: formData
- }
- );
+ const response = await apiFetch(`/admin/teams/${team.id}`, {
+ method: 'PUT',
+ body: formData
+ });
if (response.ok) {
await mutate('/admin/teams?extraFields=deletable');
diff --git a/apps/admin/src/app/[locale]/(dashboard)/teams/components/team-form.tsx b/apps/admin/src/app/[locale]/(dashboard)/teams/components/team-form.tsx
index 4386c2010..f26c057ca 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/teams/components/team-form.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/teams/components/team-form.tsx
@@ -208,9 +208,13 @@ export const TeamForm: React.FC = ({
{status && {t(`errors.${status}`)}}
-
+
+ }}
+ >
{t('title')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/users/components/create-user-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/users/components/create-user-dialog.tsx
index f61522c7d..b92cf5055 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/users/components/create-user-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/users/components/create-user-dialog.tsx
@@ -177,9 +177,13 @@ const CreationForm: React.FC = ({ onSuccess }) => {
{status && {t(`errors.${status}`)}}
-
+
= ({
label={label}
error={touched && !!error}
helperText={
- touched && error && error !== 'password-invalid'
- ? t(`errors.${error}`)
- : undefined
+ touched && error && error !== 'password-invalid' ? t(`errors.${error}`) : undefined
}
placeholder={placeholder}
fullWidth
disabled={disabled}
/>
{showRequirements && (
-
+
)}
);
};
-export { validatePassword } from './password-validation-indicator';
\ No newline at end of file
+export { validatePassword } from './password-validation-indicator';
diff --git a/apps/admin/src/app/[locale]/(dashboard)/users/components/password-validation-indicator.tsx b/apps/admin/src/app/[locale]/(dashboard)/users/components/password-validation-indicator.tsx
index 38d794f55..a83971121 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/users/components/password-validation-indicator.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/users/components/password-validation-indicator.tsx
@@ -42,9 +42,10 @@ export const PasswordRequirements: React.FC<{
+ }}
+ >
{t('title')}
diff --git a/apps/admin/src/app/[locale]/(dashboard)/users/components/permissions-editor-dialog.tsx b/apps/admin/src/app/[locale]/(dashboard)/users/components/permissions-editor-dialog.tsx
index e887531c1..8ecc30c05 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/users/components/permissions-editor-dialog.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/users/components/permissions-editor-dialog.tsx
@@ -95,9 +95,10 @@ const PermissionsForm: React.FC = ({ userId, onClose }) =>
+ }}
+ >
{t('description')}
@@ -110,7 +111,7 @@ const PermissionsForm: React.FC = ({ userId, onClose }) =>
{ALL_ADMIN_PERMISSIONS.map(permission => {
const isEditor = user.id === userId;
- const isManageUsers = permission === "MANAGE_USERS";
+ const isManageUsers = permission === 'MANAGE_USERS';
return (
= ({ userId, onClose }) =>
/>
}
label={getPermissionName(permission)}
- title={isEditor && isManageUsers ? t('errors.cannot-remove-own-manage-users') : undefined}
+ title={
+ isEditor && isManageUsers ? t('errors.cannot-remove-own-manage-users') : undefined
+ }
/>
);
})}
@@ -168,10 +171,11 @@ export const PermissionsEditorDialog: React.FC = (
+ }}
+ >
diff --git a/apps/admin/src/app/[locale]/(dashboard)/users/page.tsx b/apps/admin/src/app/[locale]/(dashboard)/users/page.tsx
index 896ab18dc..d5898129b 100644
--- a/apps/admin/src/app/[locale]/(dashboard)/users/page.tsx
+++ b/apps/admin/src/app/[locale]/(dashboard)/users/page.tsx
@@ -27,10 +27,11 @@ export default async function UsersPage() {
+ }}
+ >
{t('title')}
diff --git a/apps/admin/src/app/[locale]/login/login-form.tsx b/apps/admin/src/app/[locale]/login/login-form.tsx
index 1f163b41c..e70993ead 100644
--- a/apps/admin/src/app/[locale]/login/login-form.tsx
+++ b/apps/admin/src/app/[locale]/login/login-form.tsx
@@ -101,8 +101,8 @@ export function LoginForm({ recaptchaRequired }: LoginFormProps) {
}
router.push(decodeURIComponent(returnUrl));
} catch (error) {
- const message = error instanceof Error ? error.message : 'server-error';
- setStatus(message);
+ const message = error instanceof Error ? error.message : 'server-error';
+ setStatus(message);
}
};
@@ -121,9 +121,10 @@ export function LoginForm({ recaptchaRequired }: LoginFormProps) {
+ }}
+ >
{t('title')}
@@ -166,10 +167,11 @@ export function LoginForm({ recaptchaRequired }: LoginFormProps) {
+ display: 'flex',
+ justifyContent: 'center',
+ width: '100%'
+ }}
+ >
+ }}
+ >
{t('subtitle')}
diff --git a/apps/admin/src/app/error.tsx b/apps/admin/src/app/error.tsx
index e6584fa40..aca5c3b1c 100644
--- a/apps/admin/src/app/error.tsx
+++ b/apps/admin/src/app/error.tsx
@@ -25,17 +25,22 @@ export default function ErrorPage({ error, reset }: ErrorProps) {
}}
>
-
+
Oops! Something went wrong, please try again.
+ }}
+ >
{error.message || 'Unknown error occurred'}
0) {
const teamIdsSet = new Set(args.teamIds);
- matches = matches.filter(match => match.participants.some(p => teamIdsSet.has(p.team_id)));
+ matches = matches.filter(match =>
+ match.participants.some(p => p.team_id != null && teamIdsSet.has(p.team_id))
+ );
}
// Fetch state data from MongoDB for all matches
diff --git a/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-open-rubrics-during-session.ts b/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-open-rubrics-during-session.ts
index e9a0a6606..9261961f6 100644
--- a/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-open-rubrics-during-session.ts
+++ b/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-open-rubrics-during-session.ts
@@ -25,7 +25,11 @@ export const judgingOpenRubricsDuringSessionResolver: GraphQLFieldResolver<
const eventSettings = await db.events.byId(division.event_id).getSettings();
return eventSettings?.open_rubrics_during_session ?? false;
} catch (error) {
- console.error('Error resolving openRubricsDuringSession for division:', judging.divisionId, error);
+ console.error(
+ 'Error resolving openRubricsDuringSession for division:',
+ judging.divisionId,
+ error
+ );
throw error;
}
};
diff --git a/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-session-length.ts b/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-session-length.ts
index 8eacf4e65..13d6a14c7 100644
--- a/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-session-length.ts
+++ b/apps/backend/src/lib/graphql/resolvers/divisions/judging/judging-session-length.ts
@@ -22,6 +22,12 @@ export const judgingSessionLengthResolver: GraphQLFieldResolver<
throw new Error(`Division not found for division ID: ${judging.divisionId}`);
}
+ if (!division?.schedule_settings) {
+ throw new Error(
+ `Division schedule settings not found for division ID: ${judging.divisionId}`
+ );
+ }
+
return division.schedule_settings.judging_session_length;
} catch (error) {
console.error('Error fetching judging rooms for division:', judging.divisionId, error);
diff --git a/apps/backend/src/lib/graphql/resolvers/events/resolver.ts b/apps/backend/src/lib/graphql/resolvers/events/resolver.ts
index 254c0ed5d..6feb0f201 100644
--- a/apps/backend/src/lib/graphql/resolvers/events/resolver.ts
+++ b/apps/backend/src/lib/graphql/resolvers/events/resolver.ts
@@ -2,6 +2,7 @@ import { GraphQLFieldResolver } from 'graphql';
import { sql } from 'kysely';
import dayjs from 'dayjs';
import { Event } from '@lems/database';
+import { PortalEventResponseSchema } from '@lems/types/api/portal';
import db from '../../../database';
export interface EventGraphQL {
@@ -68,14 +69,31 @@ function buildEventQuery(args: EventsArgs) {
.selectFrom('events')
.leftJoin('divisions', 'divisions.event_id', 'events.id')
.leftJoin('event_settings', 'event_settings.event_id', 'events.id')
- .select(['events.id', 'events.slug', 'events.name', 'events.start_date', 'events.end_date', 'events.region', 'events.timezone'])
+ .select([
+ 'events.id',
+ 'events.slug',
+ 'events.name',
+ 'events.start_date',
+ 'events.end_date',
+ 'events.region',
+ 'events.timezone'
+ ])
.select(
sql`COALESCE(BOOL_AND(divisions.has_awards AND divisions.has_users AND divisions.has_schedule), false)`.as(
'is_fully_set_up'
)
)
.select('event_settings.official')
- .groupBy(['events.id', 'events.slug', 'events.name', 'events.start_date', 'events.end_date', 'events.region', 'events.timezone', 'event_settings.official']);
+ .groupBy([
+ 'events.id',
+ 'events.slug',
+ 'events.name',
+ 'events.start_date',
+ 'events.end_date',
+ 'events.region',
+ 'events.timezone',
+ 'event_settings.official'
+ ]);
// Apply date filters
if (args.startAfter) {
@@ -107,15 +125,20 @@ function buildEventQuery(args: EventsArgs) {
* Converts database date format to ISO strings for GraphQL.
* Optionally includes isFullySetUp if provided (e.g., from aggregated queries).
*/
-function buildResult(event: Partial & { is_fully_set_up?: boolean; official?: boolean }): EventGraphQL {
+function buildResult(
+ event: Partial & { is_fully_set_up?: boolean; official?: boolean | null }
+): EventGraphQL {
+ // Validate with zod schema
+ const validatedEvent = PortalEventResponseSchema.parse(event);
+
return {
- id: event.id,
- slug: event.slug,
- name: event.name,
- startDate: event.start_date.toISOString(),
- endDate: event.end_date.toISOString(),
- region: event.region,
- timezone: event.timezone,
+ id: validatedEvent.id,
+ slug: validatedEvent.slug,
+ name: validatedEvent.name,
+ startDate: validatedEvent.startDate.toISOString(),
+ endDate: validatedEvent.endDate.toISOString(),
+ region: validatedEvent.region,
+ timezone: validatedEvent.timezone,
isFullySetUp: event.is_fully_set_up,
official: event.official ?? true
};
diff --git a/apps/backend/src/lib/graphql/resolvers/index.ts b/apps/backend/src/lib/graphql/resolvers/index.ts
index f5b4279d1..84a7cefba 100644
--- a/apps/backend/src/lib/graphql/resolvers/index.ts
+++ b/apps/backend/src/lib/graphql/resolvers/index.ts
@@ -1,4 +1,4 @@
-import { GraphQLScalarType, Kind } from 'graphql';
+import { GraphQLScalarType, Kind, ValueNode } from 'graphql';
import db from '../../database';
import { eventResolvers } from './events/resolver';
import { divisionResolver } from './divisions/resolver';
@@ -60,31 +60,33 @@ import {
} from './subscriptions/deliberations';
// JSON scalar resolver - passes through any valid JSON value
+function parseJsonLiteral(ast: ValueNode): unknown {
+ switch (ast.kind) {
+ case Kind.STRING:
+ case Kind.BOOLEAN:
+ return ast.value;
+ case Kind.INT:
+ case Kind.FLOAT:
+ return parseFloat(ast.value);
+ case Kind.OBJECT:
+ return Object.fromEntries(
+ ast.fields.map(field => [field.name.value, parseJsonLiteral(field.value)])
+ );
+ case Kind.LIST:
+ return ast.values.map(value => parseJsonLiteral(value));
+ case Kind.NULL:
+ return null;
+ default:
+ return null;
+ }
+}
+
const JSONScalar = new GraphQLScalarType({
name: 'JSON',
description: 'Arbitrary JSON value',
serialize: (value: unknown) => value,
parseValue: (value: unknown) => value,
- parseLiteral: ast => {
- switch (ast.kind) {
- case Kind.STRING:
- case Kind.BOOLEAN:
- return ast.value;
- case Kind.INT:
- case Kind.FLOAT:
- return parseFloat(ast.value);
- case Kind.OBJECT:
- return Object.fromEntries(
- ast.fields.map(field => [field.name.value, JSONScalar.parseLiteral(field.value)])
- );
- case Kind.LIST:
- return ast.values.map(value => JSONScalar.parseLiteral(value));
- case Kind.NULL:
- return null;
- default:
- return null;
- }
- }
+ parseLiteral: parseJsonLiteral
});
export const resolvers = {
@@ -98,7 +100,7 @@ export const resolvers = {
Subscription: subscriptionResolvers,
Event: {
isFullySetUp: isFullySetUpResolver,
- seasonName: async event => {
+ seasonName: async (event: { id: string }) => {
const dbEvent = await db.events.byId(event.id).get();
if (!dbEvent) {
return null;
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/audience-display/update-presentation.ts b/apps/backend/src/lib/graphql/resolvers/mutations/audience-display/update-presentation.ts
index 7a865c9cb..5281cf009 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/audience-display/update-presentation.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/audience-display/update-presentation.ts
@@ -55,20 +55,22 @@ export const updatePresentationResolver: GraphQLFieldResolver<
{ returnDocument: 'after' }
);
- if (!result) {
+ if (!result?.audienceDisplay?.awardsPresentation) {
throw new MutationError(
MutationErrorCode.INTERNAL_ERROR,
`Failed to update audience display for ${divisionId}`
);
}
+ const awardsPresentation = result.audienceDisplay.awardsPresentation;
+
// Publish event to notify subscribers
const pubSub = getRedisPubSub();
await pubSub.publish(divisionId, RedisEventTypes.AWARDS_PRESENTATION_UPDATED, {
- awardsPresentation: result.audienceDisplay.awardsPresentation
+ awardsPresentation
});
- return { awardsPresentation: result.audienceDisplay.awardsPresentation };
+ return { awardsPresentation };
} catch (error) {
throw error instanceof Error ? error : new Error(String(error));
}
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts
index 1bc2ae1a9..2b0a78a4d 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/advance-final-deliberation-stage.ts
@@ -93,6 +93,15 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
nextStage = STAGE_PROGRESSION[nextStage];
}
+ if (!nextStage) {
+ throw new MutationError(
+ MutationErrorCode.FORBIDDEN,
+ 'Cannot advance beyond review stage. Use completeFinalDeliberation instead.'
+ );
+ }
+
+ const stageToAdvance = nextStage;
+
// Handle stage specific advancement logic
switch (deliberation.stage) {
case 'champions':
@@ -113,11 +122,11 @@ export const advanceFinalDeliberationStageResolver: GraphQLFieldResolver<
// Update to next stage and clear stage-specific data
const updated = await db.finalDeliberations.byDivision(divisionId).update({
- stage: nextStage,
+ stage: stageToAdvance,
status: 'not-started',
stageData: {
...deliberation.stageData,
- [nextStage]: {}
+ [stageToAdvance]: {}
}
});
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/complete-final-deliberation.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/complete-final-deliberation.ts
index 181bb723d..d10730ffb 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/complete-final-deliberation.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/complete-final-deliberation.ts
@@ -115,9 +115,10 @@ function validateFinalAwards(deliberation: { awards: FinalDeliberationAwards }):
}
// Validate core awards
- const requiredCoreAwards = ['innovation-project', 'robot-design', 'core-values'];
+ const requiredCoreAwards = ['innovation-project', 'robot-design', 'core-values'] as const;
for (const awardName of requiredCoreAwards) {
- if (!awards[awardName] || awards[awardName].length === 0) {
+ const winners = awards[awardName];
+ if (!winners || winners.length === 0) {
throw new MutationError(MutationErrorCode.FORBIDDEN, `${awardName} award must be assigned`);
}
}
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/champions.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/champions.ts
index 3eed3de66..1ee44be12 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/champions.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/champions.ts
@@ -100,7 +100,7 @@ export async function handleChampionsStageCompletion(
const advancingTeamIds = selectAdvancingTeams(
teamsWithRanks,
- Object.values(champions),
+ Object.values(champions ?? {}),
advancementConfig.advancement_percent,
teams.length
);
@@ -142,7 +142,8 @@ const assignChampionsToTeams = async (
): Promise => {
const championsAwards = await db.awards.byDivisionId(divisionId).get('champions');
for (const award of championsAwards) {
- const teamId = champions[award.place];
+ const placeKey = String(award.place) as '1' | '2' | '3' | '4';
+ const teamId = champions?.[placeKey];
if (!teamId) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/core-awards.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/core-awards.ts
index 4a10a8c92..d4ee5ab27 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/core-awards.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/core-awards.ts
@@ -62,15 +62,19 @@ export async function handleCoreAwardsStageCompletion(
const teamScores = await calculateAllTeamScores(divisionId, teams);
const teamsWithRanks = await rankTeams(teamScores, divisionId);
+ const innovationProjectWinners = awards['innovation-project'] ?? [];
+ const robotDesignWinners = awards['robot-design'] ?? [];
+ const coreValuesWinners = awards['core-values'] ?? [];
+
const excellenceInEngineeringWinners = teamsWithRanks
.filter(
t =>
!Object.values(awards.champions || {}).includes(t.teamId) &&
- !awards['innovation-project'].includes(t.teamId) &&
- !awards['robot-design'].includes(t.teamId) &&
- !awards['core-values'].includes(t.teamId)
+ !innovationProjectWinners.includes(t.teamId) &&
+ !robotDesignWinners.includes(t.teamId) &&
+ !coreValuesWinners.includes(t.teamId)
)
- .sort((a, b) => a.ranks['total'] - b.ranks['total'])
+ .sort((a, b) => a.averageRank - b.averageRank)
.slice(0, excellenceInEngineeringAwards.length)
.map(t => t.teamId);
@@ -89,7 +93,8 @@ export async function handleCoreAwardsStageCompletion(
async function validateCoreAwardsAssignment(awards: FinalDeliberationAwards): Promise {
const championsIds = Object.values(awards.champions || {});
for (const category of categories) {
- if (awards[category].filter(winnerId => championsIds.includes(winnerId)).length > 0) {
+ const categoryWinners = awards[category] ?? [];
+ if (categoryWinners.filter(winnerId => championsIds.includes(winnerId)).length > 0) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
`Category ${category} has a winner that was already assigned a champions award`
@@ -109,7 +114,8 @@ async function assignCoreAwardsToTeams(
const categoryAwards = await db.awards.byDivisionId(divisionId).get(category);
for (let i = 0; i < categoryAwards.length; i++) {
const award = categoryAwards[i];
- const teamId = awards[category][i];
+ const categoryWinners = awards[category] ?? [];
+ const teamId = categoryWinners[i];
if (!teamId) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts
index 179654f18..b44b7c076 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/optional-awards.ts
@@ -11,6 +11,7 @@ export function validateOptionalAwardsStage(
optionalAwards: FinalDeliberationAwards['optionalAwards'],
divisionAwards: Award[]
): Promise {
+ const resolvedOptionalAwards = optionalAwards ?? {};
const divisionOptionalAwards = divisionAwards.filter(
award =>
(OPTIONAL_AWARDS as readonly string[])
@@ -18,12 +19,13 @@ export function validateOptionalAwardsStage(
.includes(award.name) && award.type === 'TEAM'
);
for (const award of divisionOptionalAwards) {
- if (!optionalAwards[award.name] || optionalAwards[award.name].length === 0) {
+ if (!resolvedOptionalAwards[award.name] || resolvedOptionalAwards[award.name].length === 0) {
return Promise.reject(
new Error(`Optional award "${award.name}" must have at least one team assigned.`)
);
}
}
+ return Promise.resolve();
}
/**
@@ -51,9 +53,9 @@ async function validateOptionalAwardsAssignment(
const awards = (await db.awards.byDivisionId(divisionId).getAll()).filter(
award => award.name !== 'robot-performance'
);
- for (const [awardName, winners] of Object.entries(optionalAwards)) {
+ for (const [awardName, winners] of Object.entries(optionalAwards ?? {})) {
for (const award of awards) {
- if (winners.includes(award.winner_id)) {
+ if (award.winner_id && winners.includes(award.winner_id)) {
throw new MutationError(
MutationErrorCode.FORBIDDEN,
`Award ${awardName} has a winner that was already assigned an award`
@@ -70,7 +72,7 @@ async function assignOptionalAwardsToTeams(
divisionId: string,
optionalAwards: FinalDeliberationAwards['optionalAwards']
): Promise {
- for (const [awardName, teamIds] of Object.entries(optionalAwards)) {
+ for (const [awardName, teamIds] of Object.entries(optionalAwards ?? {})) {
const awards = await db.awards.byDivisionId(divisionId).get(awardName);
if (awards.length !== teamIds.length) {
throw new MutationError(
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/utils.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/utils.ts
index 669809709..b6479abdf 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/utils.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/handlers/utils.ts
@@ -68,7 +68,7 @@ function calculateRubricScores(
teamId: string,
teamRubrics: Array<{
teamId: string;
- data?: { fields?: Record };
+ data?: { fields?: Record };
category: JudgingCategory;
}>
): Record {
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/start-deliberation.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/start-deliberation.ts
index 507a90307..e2a502f6a 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/start-deliberation.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/start-deliberation.ts
@@ -55,6 +55,13 @@ export const startDeliberationResolver: GraphQLFieldResolver<
);
}
+ if (!updated.start_time) {
+ throw new MutationError(
+ MutationErrorCode.INTERNAL_ERROR,
+ `Deliberation start time missing for division ${divisionId}`
+ );
+ }
+
const pubSub = getRedisPubSub();
await Promise.all([
pubSub.publish(divisionId, RedisEventTypes.DELIBERATION_UPDATED, {
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts
index 3ff8309ad..b9dcc6a14 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/deliberations/update-final-deliberation-awards.ts
@@ -2,6 +2,7 @@ import { GraphQLFieldResolver } from 'graphql';
import { RedisEventTypes } from '@lems/types/api/lems/redis';
import { MutationError, MutationErrorCode } from '@lems/types/api/lems';
import { Award, OPTIONAL_AWARDS } from '@lems/shared/awards';
+import { FinalDeliberationAwards } from '@lems/database';
import type { GraphQLContext } from '../../../apollo-server';
import db from '../../../../database';
import { getRedisPubSub } from '../../../../redis/redis-pubsub';
@@ -85,12 +86,12 @@ export const updateFinalDeliberationAwardsResolver: GraphQLFieldResolver<
const updatedAwards = {
...deliberation.awards,
optionalAwards: {
- ...deliberation.awards.optionalAwards, // Preserve existing optional awards
- ...optionalAwards // Apply incoming updates
+ ...deliberation.awards.optionalAwards,
+ ...optionalAwards
},
- ...mandatoryAwards, // Mandatory awards already handle all fields in this update
- ...(Object.keys(championsAward).length > 0 && { champions: championsAward })
- };
+ ...mandatoryAwards,
+ ...(Object.keys(championsAward).length > 0 ? { champions: championsAward } : {})
+ } as FinalDeliberationAwards;
// Update the deliberation
const updated = await db.finalDeliberations.byDivision(divisionId).update({
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/judging-sessions/start-judging-session.ts b/apps/backend/src/lib/graphql/resolvers/mutations/judging-sessions/start-judging-session.ts
index fc7ade348..c2cb4136b 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/judging-sessions/start-judging-session.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/judging-sessions/start-judging-session.ts
@@ -32,6 +32,12 @@ export const startJudgingSessionResolver: GraphQLFieldResolver<
await checkSessionCanBeStarted(divisionId, session);
const division = await db.divisions.byId(divisionId).get();
+ if (!division?.schedule_settings) {
+ throw new MutationError(
+ MutationErrorCode.INTERNAL_ERROR,
+ `Division schedule settings not found for division ${divisionId}`
+ );
+ }
const scheduledTime = dayjs(session.scheduled_time);
const startTime = new Date();
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/rubrics/utils.ts b/apps/backend/src/lib/graphql/resolvers/mutations/rubrics/utils.ts
index 0a175a48d..8b78c9720 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/rubrics/utils.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/rubrics/utils.ts
@@ -166,8 +166,7 @@ export function determineRubricCompletionStatus(
}
const fields = rubricData.fields as
- | Record
- | undefined;
+ Record | undefined;
const feedback = rubricData.feedback as { greatJob?: string; thinkAbout?: string } | undefined;
let hasAnyValue = false;
@@ -225,8 +224,7 @@ export function isRubricComplete(
}
const fields = rubricData.fields as
- | Record
- | undefined;
+ Record | undefined;
const feedback = rubricData.feedback as { greatJob?: string; thinkAbout?: string } | undefined;
// Criterion 1: All fields must have non-null values
diff --git a/apps/backend/src/lib/graphql/resolvers/mutations/scoresheets/update-mission-clause.ts b/apps/backend/src/lib/graphql/resolvers/mutations/scoresheets/update-mission-clause.ts
index 5653920c6..07741d180 100644
--- a/apps/backend/src/lib/graphql/resolvers/mutations/scoresheets/update-mission-clause.ts
+++ b/apps/backend/src/lib/graphql/resolvers/mutations/scoresheets/update-mission-clause.ts
@@ -24,6 +24,8 @@ interface UpdateScoresheetMissionClauseArgs {
value: ScoresheetClauseValue;
}
+type MissionData = Record>;
+
/**
* Resolver for Mutation.updateScoresheetMissionClause
* Updates a single mission clause value in a scoresheet
@@ -62,17 +64,16 @@ export const updateScoresheetMissionClauseResolver: GraphQLFieldResolver<
validateClauseValue(clause, value);
// Calculate points
- const { data = {} } = dbScoresheet;
- data['missions'] ??= {};
- data['missions'][missionId] ??= {};
- data['missions'][missionId][clauseIndex] = value;
- const points = calculateScore(data['missions']);
+ const data = (dbScoresheet.data ?? {}) as { missions?: MissionData };
+ data.missions ??= {};
+ data.missions[missionId] ??= {};
+ data.missions[missionId][clauseIndex] = value;
+ const points = calculateScore(data.missions);
// Determine new status based on completion criteria
// Don't change status if already submitted or in gp status (locked states)
- const newStatus = (status === 'submitted' || status === 'gp')
- ? status
- : determineScoresheetCompletionStatus(data);
+ const newStatus =
+ status === 'submitted' || status === 'gp' ? status : determineScoresheetCompletionStatus(data);
const updateFields: Record = {
[`data.missions.${missionId}.${clauseIndex}`]: value,
diff --git a/apps/backend/src/lib/graphql/utils/rubric-builder.ts b/apps/backend/src/lib/graphql/utils/rubric-builder.ts
index 8772c5c83..a16325c4d 100644
--- a/apps/backend/src/lib/graphql/utils/rubric-builder.ts
+++ b/apps/backend/src/lib/graphql/utils/rubric-builder.ts
@@ -13,7 +13,7 @@ export interface RubricGraphQL {
status: string;
data?: {
awards?: Record;
- fields: Record;
+ fields: Record;
feedback?: { greatJob: string; thinkAbout: string };
};
}
diff --git a/apps/backend/src/lib/queues/types.ts b/apps/backend/src/lib/queues/types.ts
index 09c79b06c..21fa04b39 100644
--- a/apps/backend/src/lib/queues/types.ts
+++ b/apps/backend/src/lib/queues/types.ts
@@ -1,7 +1,5 @@
export type ScheduledEventType =
- | 'session-completed'
- | 'match-completed'
- | 'match-endgame-triggered';
+ 'session-completed' | 'match-completed' | 'match-endgame-triggered';
export interface ScheduledEvent {
eventType: ScheduledEventType;
diff --git a/apps/backend/src/lib/redis/redis-pubsub.ts b/apps/backend/src/lib/redis/redis-pubsub.ts
index 8f1b8ba04..4e0a6c0c6 100644
--- a/apps/backend/src/lib/redis/redis-pubsub.ts
+++ b/apps/backend/src/lib/redis/redis-pubsub.ts
@@ -130,7 +130,8 @@ export class RedisPubSub {
isActive = false;
if (timeoutHandle) clearTimeout(timeoutHandle);
broadcaster.removeListener('event', messageHandler);
- if (resolveWait) resolveWait();
+ // Typescript thinks resolveWait is of type never, so we need to cast it to a function
+ if (resolveWait) (resolveWait as () => void)();
broadcaster.decrementSubscribers();
}
}
diff --git a/apps/backend/src/lib/security/credentials.ts b/apps/backend/src/lib/security/credentials.ts
index b14f8a581..fce0ec322 100644
--- a/apps/backend/src/lib/security/credentials.ts
+++ b/apps/backend/src/lib/security/credentials.ts
@@ -11,10 +11,7 @@ export async function hashPassword(password: string): Promise {
return { hash };
}
-export async function verifyPassword(
- password: string,
- storedHash: string
-): Promise {
+export async function verifyPassword(password: string, storedHash: string): Promise {
return bcrypt.compare(password, storedHash);
}
diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts
index e6726eb27..e3c236727 100644
--- a/apps/backend/src/main.ts
+++ b/apps/backend/src/main.ts
@@ -1,6 +1,6 @@
import * as http from 'http';
import * as path from 'path';
-import express from 'express';
+import express, { ErrorRequestHandler, NextFunction, Request, Response } from 'express';
import morgan from 'morgan';
import favicon from 'serve-favicon';
import cookies from 'cookie-parser';
@@ -8,7 +8,7 @@ import cors from 'cors';
import { expressMiddleware } from '@as-integrations/express5';
import timesyncServer from 'timesync/server';
import { WebSocketServer } from 'ws';
-import { useServer } from 'graphql-ws/use/ws';
+import { useServer as createGraphqlWsServer } from 'graphql-ws/use/ws';
import './lib/dayjs';
import './lib/database';
import { logger } from './lib/logger';
@@ -98,7 +98,7 @@ const wsServer = new WebSocketServer({
path: '/lems/graphql'
});
-const serverCleanup = useServer(
+const serverCleanup = createGraphqlWsServer(
{
schema,
context: async (ctx): Promise => {
@@ -156,7 +156,7 @@ app.use((req, res) => {
// Error handler
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-app.use((err, req, res, next) => {
+app.use(((err: Error, req: Request, res: Response, _next: NextFunction) => {
logger.error(
{
component: 'http',
@@ -168,7 +168,7 @@ app.use((err, req, res, next) => {
'Unhandled error'
);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
-});
+}) as ErrorRequestHandler);
logger.info('Starting server...');
const port = 3333;
diff --git a/apps/backend/src/routers/admin/auth.ts b/apps/backend/src/routers/admin/auth.ts
index f2cbb6d72..7c2700149 100644
--- a/apps/backend/src/routers/admin/auth.ts
+++ b/apps/backend/src/routers/admin/auth.ts
@@ -8,6 +8,7 @@ import db from '../../lib/database';
import { getRecaptchaResponse } from '../../lib/security/captcha';
import { verifyPassword } from '../../lib/security/credentials';
import { logger } from '../../lib/logger';
+import { asHandler } from '../../types/express-handlers';
import { makeAdminUserResponse } from './users/util';
const router = express.Router({ mergeParams: true });
@@ -34,15 +35,25 @@ router.post('/login', loginRateLimiter, async (req: Request, res: Response, next
const { captchaToken, ...loginDetails }: LoginRequest = req.body;
if (process.env.RECAPTCHA === 'true') {
+ if (!captchaToken) {
+ res.status(400).json({ error: 'CAPTCHA_REQUIRED' });
+ return;
+ }
const captcha: RecaptchaResponse = await getRecaptchaResponse(captchaToken);
- if (!captcha.success || captcha['error-codes']?.length > 0) {
- logger.warn({ component: 'auth', action: 'login', errorCodes: captcha['error-codes'] || [] }, 'Captcha failure');
+ if (!captcha.success || (captcha['error-codes']?.length ?? 0) > 0) {
+ logger.warn(
+ { component: 'auth', action: 'login', errorCodes: captcha['error-codes'] || [] },
+ 'Captcha failure'
+ );
res.status(500).json({ error: 'CAPTCHA_FAILED' });
return;
}
if (captcha.action != 'submit' || captcha.score < 0.5) {
- logger.warn({ component: 'auth', action: 'login', score: captcha.score }, 'Captcha score too low');
+ logger.warn(
+ { component: 'auth', action: 'login', score: captcha.score },
+ 'Captcha score too low'
+ );
res.status(429).json({ error: 'TOO_MANY_REQUESTS' });
return;
}
@@ -57,7 +68,15 @@ router.post('/login', loginRateLimiter, async (req: Request, res: Response, next
const adminUser = await db.admins.byUsername(loginDetails.username).get();
if (!adminUser) {
- logger.warn({ component: 'auth', action: 'login', username: loginDetails.username, reason: 'user_not_found' }, 'Admin login failed - user not found');
+ logger.warn(
+ {
+ component: 'auth',
+ action: 'login',
+ username: loginDetails.username,
+ reason: 'user_not_found'
+ },
+ 'Admin login failed - user not found'
+ );
res.status(401).json({ error: 'INVALID_CREDENTIALS' });
return;
}
@@ -65,12 +84,24 @@ router.post('/login', loginRateLimiter, async (req: Request, res: Response, next
const isValidPassword = await verifyPassword(loginDetails.password, adminUser.password_hash);
if (!isValidPassword) {
- logger.warn({ component: 'auth', action: 'login', username: loginDetails.username, userId: adminUser.id, reason: 'invalid_password' }, 'Admin login failed - invalid password');
+ logger.warn(
+ {
+ component: 'auth',
+ action: 'login',
+ username: loginDetails.username,
+ userId: adminUser.id,
+ reason: 'invalid_password'
+ },
+ 'Admin login failed - invalid password'
+ );
res.status(401).json({ error: 'INVALID_CREDENTIALS' });
return;
}
- logger.info({ component: 'auth', action: 'login', username: loginDetails.username, userId: adminUser.id }, 'Admin login successful');
+ logger.info(
+ { component: 'auth', action: 'login', username: loginDetails.username, userId: adminUser.id },
+ 'Admin login successful'
+ );
await db.admins.byId(adminUser.id).updateLastLogin();
const expires = dayjs().add(7, 'days');
@@ -104,25 +135,38 @@ router.post('/login', loginRateLimiter, async (req: Request, res: Response, next
loginTime: new Date()
});
} catch (err) {
- logger.error({ component: 'auth', action: 'login', error: err instanceof Error ? err.message : String(err) }, 'Admin login error');
+ logger.error(
+ {
+ component: 'auth',
+ action: 'login',
+ error: err instanceof Error ? err.message : String(err)
+ },
+ 'Admin login error'
+ );
next(err);
}
});
router.post('/logout', (req: Request, res: Response) => {
- logger.info({ component: 'auth', action: 'logout', userType: 'admin' }, 'Admin logout successful');
+ logger.info(
+ { component: 'auth', action: 'logout', userType: 'admin' },
+ 'Admin logout successful'
+ );
res.clearCookie('admin-auth-token');
res.json({ ok: true });
});
-router.get('/verify', async (req: AdminRequest, res) => {
- const user = await db.admins.byId(req.userId!).get();
- if (!user) {
- res.status(401).json({ error: 'UNAUTHORIZED' });
- return;
- }
+router.get(
+ '/verify',
+ asHandler(async (req, res) => {
+ const user = await db.admins.byId(req.userId!).get();
+ if (!user) {
+ res.status(401).json({ error: 'UNAUTHORIZED' });
+ return;
+ }
- res.json({ ok: true, user: makeAdminUserResponse(user) });
-});
+ res.json({ ok: true, user: makeAdminUserResponse(user) });
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/awards/index.ts b/apps/backend/src/routers/admin/events/divisions/awards/index.ts
index fb41087bf..d7a0a746c 100644
--- a/apps/backend/src/routers/admin/events/divisions/awards/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/awards/index.ts
@@ -2,19 +2,23 @@ import express from 'express';
import db from '../../../../../lib/database';
import { requirePermission } from '../../../middleware/require-permission';
import { AdminDivisionRequest } from '../../../../../types/express';
+import { asHandler } from '../../../../../types/express-handlers';
import { makeAdminAwardResponse } from './utils';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminDivisionRequest, res) => {
- const awards = await db.awards.byDivisionId(req.divisionId).getAll();
- res.status(200).json(awards.map(award => makeAdminAwardResponse(award)));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const awards = await db.awards.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(awards.map(award => makeAdminAwardResponse(award)));
+ })
+);
router.post(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const awards = req.body.awards;
if (!Array.isArray(awards)) {
res.status(400).json({ error: 'Awards array is required' });
@@ -50,17 +54,17 @@ router.post(
await db.divisions.byId(req.divisionId).update({ has_awards: true });
res.status(201).end();
- }
+ })
);
router.delete(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
await db.awards.byDivisionId(req.divisionId).deleteAll();
await db.divisions.byId(req.divisionId).update({ has_awards: false });
res.status(200).end();
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/index.ts b/apps/backend/src/routers/admin/events/divisions/index.ts
index deb03ce9e..3dcdeac39 100644
--- a/apps/backend/src/routers/admin/events/divisions/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/index.ts
@@ -3,6 +3,7 @@ import db from '../../../../lib/database';
import { AdminDivisionRequest, AdminEventRequest } from '../../../../types/express';
import { attachDivision } from '../../middleware/attach-division';
import { requirePermission } from '../../middleware/require-permission';
+import { asHandler } from '../../../../types/express-handlers';
import { makeAdminDivisionResponse } from './util';
import divisionRoomsRouter from './rooms';
import divisionTablesRouter from './tables';
@@ -13,23 +14,30 @@ import divisionAwardsRouter from './awards';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminEventRequest, res) => {
- const divisions = await db.divisions.byEventId(req.eventId).getAll();
- res.json(divisions.map(division => makeAdminDivisionResponse(division)));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const divisions = await db.divisions.byEventId(req.eventId).getAll();
+ res.json(divisions.map(division => makeAdminDivisionResponse(division)));
+ })
+);
-router.post('/', requirePermission('MANAGE_EVENT_DETAILS'), async (req: AdminEventRequest, res) => {
- const { name, color } = req.body;
+router.post(
+ '/',
+ requirePermission('MANAGE_EVENT_DETAILS'),
+ asHandler(async (req, res) => {
+ const { name, color } = req.body;
- if (!name || !color) {
- res.status(400).json({ error: 'Name and color are required' });
- return;
- }
+ if (!name || !color) {
+ res.status(400).json({ error: 'Name and color are required' });
+ return;
+ }
- const division = await db.divisions.create({ name, color, event_id: req.eventId });
+ const division = await db.divisions.create({ name, color, event_id: req.eventId });
- res.status(201).json(makeAdminDivisionResponse(division));
-});
+ res.status(201).json(makeAdminDivisionResponse(division));
+ })
+);
router.use('/:divisionId', attachDivision());
@@ -43,7 +51,7 @@ router.use('/:divisionId/schedule', divisionScheduleRouter);
router.put(
'/:divisionId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { name, color } = req.body;
// Name can be empty, but has to exist
@@ -55,13 +63,13 @@ router.put(
await db.divisions.byId(req.divisionId).update({ name, color });
res.status(200).end();
- }
+ })
);
router.delete(
'/:divisionId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const deleted = await db.divisions.byId(req.divisionId).delete();
if (!deleted) {
@@ -70,7 +78,7 @@ router.delete(
}
res.status(204).end();
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/pit-map/index.ts b/apps/backend/src/routers/admin/events/divisions/pit-map/index.ts
index 8fcd62494..302bca294 100644
--- a/apps/backend/src/routers/admin/events/divisions/pit-map/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/pit-map/index.ts
@@ -3,6 +3,7 @@ import fileUpload from 'express-fileupload';
import db from '../../../../../lib/database';
import { requirePermission } from '../../../middleware/require-permission';
import { AdminDivisionRequest } from '../../../../../types/express';
+import { asHandler } from '../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
@@ -10,7 +11,7 @@ router.post(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
fileUpload(),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
if (!req.files || !req.files.pitMap) {
res.status(400).json({ error: 'No pit map file provided' });
return;
@@ -42,13 +43,13 @@ router.post(
res.status(500).json({ error: 'Failed to upload pit map' });
return;
}
- }
+ })
);
router.delete(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const success = await db.divisions.byId(req.divisionId).update({ pit_map_url: null });
if (success) {
@@ -63,7 +64,7 @@ router.delete(
res.status(500).json({ error: 'Failed to delete pit map' });
return;
}
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/rooms/index.ts b/apps/backend/src/routers/admin/events/divisions/rooms/index.ts
index 9ea3c3042..61a7f612e 100644
--- a/apps/backend/src/routers/admin/events/divisions/rooms/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/rooms/index.ts
@@ -3,18 +3,22 @@ import db from '../../../../../lib/database';
import { requirePermission } from '../../../middleware/require-permission';
import { AdminDivisionRequest } from '../../../../../types/express';
import { generateVolunteerPassword } from '../../users/util';
+import { asHandler } from '../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminDivisionRequest, res) => {
- const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
- res.status(200).json(rooms);
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(rooms);
+ })
+);
router.post(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { name } = req.body;
if (!name) {
@@ -24,6 +28,10 @@ router.post(
const room = await db.rooms.create({ division_id: req.divisionId, name });
const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
const eventUser = await db.eventUsers.create({
event_id: division.event_id,
@@ -36,13 +44,13 @@ router.post(
await db.eventUsers.assignUserToDivisions(eventUser.id, [req.divisionId]);
res.status(201).end();
- }
+ })
);
router.put(
'/:roomId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { roomId } = req.params;
if (!roomId || typeof roomId !== 'string') {
res.status(400).json({ error: 'ROOM_ID_REQUIRED' });
@@ -59,13 +67,13 @@ router.put(
await db.rooms.byId(roomId).update({ name });
res.status(200).end();
- }
+ })
);
router.delete(
'/:roomId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { roomId } = req.params;
if (!roomId || typeof roomId !== 'string') {
res.status(400).json({ error: 'ROOM_ID_REQUIRED' });
@@ -81,7 +89,7 @@ router.delete(
}
res.status(204).end();
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/schedule/agenda.ts b/apps/backend/src/routers/admin/events/divisions/schedule/agenda.ts
index de5e9ab0c..0cedc1ed9 100644
--- a/apps/backend/src/routers/admin/events/divisions/schedule/agenda.ts
+++ b/apps/backend/src/routers/admin/events/divisions/schedule/agenda.ts
@@ -2,13 +2,14 @@ import express from 'express';
import db from '../../../../../lib/database';
import { AdminDivisionRequest } from '../../../../../types/express';
import { requirePermission } from '../../../middleware/require-permission';
+import { asHandler } from '../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
router.post(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const agendaEvents = req.body;
if (!Array.isArray(agendaEvents)) {
@@ -21,13 +22,13 @@ router.post(
console.error('Error updating agenda events:', error);
res.status(500).json({ error: 'Failed to update agenda events' });
}
- }
+ })
);
router.delete(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
await db.divisions.byId(req.divisionId).agenda().delete();
res.status(200).json({ ok: true });
@@ -35,7 +36,7 @@ router.delete(
console.error('Error deleting agenda event:', error);
res.status(500).json({ error: 'Failed to delete agenda event' });
}
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/schedule/index.ts b/apps/backend/src/routers/admin/events/divisions/schedule/index.ts
index 532e2c966..c3290ecf4 100644
--- a/apps/backend/src/routers/admin/events/divisions/schedule/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/schedule/index.ts
@@ -3,6 +3,7 @@ import { SchedulerRequest } from '@lems/types/api/scheduler';
import db from '../../../../../lib/database';
import { AdminDivisionRequest } from '../../../../../types/express';
import { requirePermission } from '../../../middleware/require-permission';
+import { asHandler } from '../../../../../types/express-handlers';
import {
makeAdminJudgingSessionResponse,
makeAdminJudgingRoomResponse,
@@ -20,7 +21,7 @@ router.use('/agenda', agendaRouter);
router.post(
'/validate',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const settings: SchedulerRequest = req.body;
@@ -47,13 +48,13 @@ router.post(
console.debug(error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- }
+ })
);
router.post(
'/generate',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const settings: SchedulerRequest = req.body;
@@ -80,13 +81,13 @@ router.post(
console.debug(error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- }
+ })
);
router.delete(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
await Promise.all([
db.judgingSessions.byDivision(req.divisionId).deleteAll(),
@@ -107,13 +108,13 @@ router.delete(
console.error('Error deleting division schedule:', error);
res.status(500).json({ error: 'Failed to delete division schedule' });
}
- }
+ })
);
router.get(
'/teams/:teamId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const { teamId } = req.params;
if (!teamId || typeof teamId !== 'string') {
@@ -146,13 +147,13 @@ router.get(
console.error('Error fetching team schedule:', error);
res.status(500).json({ error: 'Failed to fetch team schedule' });
}
- }
+ })
);
router.get(
'/judging-sessions',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const sessions = await db.judgingSessions.byDivision(req.divisionId).getAll();
const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
@@ -165,13 +166,13 @@ router.get(
console.error('Error fetching judging sessions:', error);
res.status(500).json({ error: 'Failed to fetch judging sessions' });
}
- }
+ })
);
router.put(
'/swap',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
try {
const { teamId1, teamId2 } = req.body;
@@ -208,7 +209,7 @@ router.put(
console.error('Error swapping team schedules:', error);
res.status(500).json({ error: 'Failed to swap team schedules' });
}
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/schedule/util.ts b/apps/backend/src/routers/admin/events/divisions/schedule/util.ts
index 101962568..bd06a35dc 100644
--- a/apps/backend/src/routers/admin/events/divisions/schedule/util.ts
+++ b/apps/backend/src/routers/admin/events/divisions/schedule/util.ts
@@ -33,13 +33,11 @@ export const makeAdminRobotGameMatchResponse = (match: DbMatchWithParticipants):
number: match.number,
stage: match.stage as RobotGameMatchStage,
scheduledTime: match.scheduled_time,
- participants: match.participants.map(
- (participant): MatchParticipant => ({
- id: participant.id,
- teamId: participant.team_id,
- tableId: participant.table_id,
- matchId: participant.match_id
- })
- )
+ participants: match.participants.map((participant): MatchParticipant => ({
+ id: participant.id,
+ teamId: participant.team_id,
+ tableId: participant.table_id,
+ matchId: participant.match_id
+ }))
};
};
diff --git a/apps/backend/src/routers/admin/events/divisions/tables/index.ts b/apps/backend/src/routers/admin/events/divisions/tables/index.ts
index a24ae96e9..dac018b0e 100644
--- a/apps/backend/src/routers/admin/events/divisions/tables/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/tables/index.ts
@@ -3,18 +3,22 @@ import db from '../../../../../lib/database';
import { requirePermission } from '../../../middleware/require-permission';
import { AdminDivisionRequest } from '../../../../../types/express';
import { generateVolunteerPassword } from '../../users/util';
+import { asHandler } from '../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminDivisionRequest, res) => {
- const tables = await db.tables.byDivisionId(req.divisionId).getAll();
- res.status(200).json(tables);
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const tables = await db.tables.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(tables);
+ })
+);
router.post(
'/',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { name } = req.body;
if (!name) {
@@ -24,6 +28,10 @@ router.post(
const table = await db.tables.create({ division_id: req.divisionId, name });
const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
const eventUser = await db.eventUsers.create({
event_id: division.event_id,
@@ -36,13 +44,13 @@ router.post(
await db.eventUsers.assignUserToDivisions(eventUser.id, [req.divisionId]);
res.status(201).end();
- }
+ })
);
router.put(
'/:tableId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { tableId } = req.params;
const { name } = req.body;
@@ -59,13 +67,13 @@ router.put(
await db.tables.byId(tableId).update({ name });
res.status(200).end();
- }
+ })
);
router.delete(
'/:tableId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminDivisionRequest, res) => {
+ asHandler(async (req, res) => {
const { tableId } = req.params;
if (!tableId || typeof tableId !== 'string') {
res.status(400).json({ error: 'TABLE_ID_REQUIRED' });
@@ -81,7 +89,7 @@ router.delete(
}
res.status(204).end();
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/divisions/teams/index.ts b/apps/backend/src/routers/admin/events/divisions/teams/index.ts
index 743c3f816..4768b6f42 100644
--- a/apps/backend/src/routers/admin/events/divisions/teams/index.ts
+++ b/apps/backend/src/routers/admin/events/divisions/teams/index.ts
@@ -2,12 +2,16 @@ import express from 'express';
import db from '../../../../../lib/database';
import { AdminDivisionRequest } from '../../../../../types/express';
import { makeAdminTeamResponse } from '../../../teams/util';
+import { asHandler } from '../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminDivisionRequest, res) => {
- const teams = await db.teams.byDivisionId(req.divisionId).getAll();
- res.json(teams.map(team => makeAdminTeamResponse(team)));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const teams = await db.teams.byDivisionId(req.divisionId).getAll();
+ res.json(teams.map(team => makeAdminTeamResponse(team)));
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/events/index.ts b/apps/backend/src/routers/admin/events/index.ts
index 9fe080898..a1fd2ebb5 100644
--- a/apps/backend/src/routers/admin/events/index.ts
+++ b/apps/backend/src/routers/admin/events/index.ts
@@ -5,6 +5,7 @@ import db from '../../../lib/database';
import { attachEvent } from '../middleware/attach-event';
import { requirePermission } from '../middleware/require-permission';
import { AdminEventRequest, AdminRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import { makeAdminEventResponse, makeAdminEventSummaryResponse } from './util';
import eventUsersRouter from './users';
import eventTeamsRouter from './teams';
@@ -14,15 +15,21 @@ import eventIntegrationsRouter from './integrations';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminEventRequest, res) => {
- const events = await db.events.getAll();
- res.json(events.map(event => makeAdminEventResponse(event)));
-});
-
-router.get('/me', async (req: AdminRequest, res) => {
- const events = await db.admins.byId(req.userId).getEvents();
- res.json(events.map(event => makeAdminEventResponse(event)));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const events = await db.events.getAll();
+ res.json(events.map(event => makeAdminEventResponse(event)));
+ })
+);
+
+router.get(
+ '/me',
+ asHandler(async (req, res) => {
+ const events = await db.admins.byId(req.userId).getEvents();
+ res.json(events.map(event => makeAdminEventResponse(event)));
+ })
+);
router.get('/slug/:slug', async (req, res) => {
const event = await db.events.bySlug(req.params.slug).get();
@@ -45,113 +52,119 @@ router.get('/season/:seasonId/summary', async (req, res) => {
res.json(events.map(event => makeAdminEventSummaryResponse(event)));
});
-router.post('/', requirePermission('MANAGE_EVENTS'), async (req: AdminRequest, res) => {
- try {
- const { name, slug, date, location, region, timezone, divisions } = req.body;
+router.post(
+ '/',
+ requirePermission('MANAGE_EVENTS'),
+ asHandler(async (req, res) => {
+ try {
+ const { name, slug, date, location, region, timezone, divisions } = req.body;
- if (!name || !slug || !date || !location || !region || !timezone) {
- res
- .status(400)
- .json({ error: 'Name, slug, date, location, region, and timezone are required' });
- return;
- }
+ if (!name || !slug || !date || !location || !region || !timezone) {
+ res
+ .status(400)
+ .json({ error: 'Name, slug, date, location, region, and timezone are required' });
+ return;
+ }
- if (typeof region !== 'string' || region.length !== 2 || !/^[A-Z]{2}$/.test(region)) {
- res.status(400).json({ error: 'Region must be a 2-letter ISO 3166-1 alpha-2 country code' });
- return;
- }
+ if (typeof region !== 'string' || region.length !== 2 || !/^[A-Z]{2}$/.test(region)) {
+ res
+ .status(400)
+ .json({ error: 'Region must be a 2-letter ISO 3166-1 alpha-2 country code' });
+ return;
+ }
- if (typeof timezone !== 'string' || !timezone.trim()) {
- res.status(400).json({ error: 'Timezone must be a valid IANA timezone string' });
- return;
- }
+ if (typeof timezone !== 'string' || !timezone.trim()) {
+ res.status(400).json({ error: 'Timezone must be a valid IANA timezone string' });
+ return;
+ }
- if (!Array.isArray(divisions) || divisions.length === 0) {
- res.status(400).json({ error: 'At least one division is required' });
- return;
- }
+ if (!Array.isArray(divisions) || divisions.length === 0) {
+ res.status(400).json({ error: 'At least one division is required' });
+ return;
+ }
- if (divisions.length > 1) {
- for (const division of divisions) {
- if (!division.name || !division.color) {
- res.status(400).json({ error: 'Each division must have a name and color' });
- return;
+ if (divisions.length > 1) {
+ for (const division of divisions) {
+ if (!division.name || !division.color) {
+ res.status(400).json({ error: 'Each division must have a name and color' });
+ return;
+ }
}
}
- }
- const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
- if (!slugPattern.test(slug)) {
- res
- .status(400)
- .json({ error: 'Invalid slug format. Use lowercase letters, numbers, and dashes only' });
- return;
- }
+ const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+ if (!slugPattern.test(slug)) {
+ res
+ .status(400)
+ .json({ error: 'Invalid slug format. Use lowercase letters, numbers, and dashes only' });
+ return;
+ }
- const existingEvent = await db.events.bySlug(slug).get();
- if (existingEvent) {
- res.status(409).json({ error: 'Event with this slug already exists' });
- return;
- }
+ const existingEvent = await db.events.bySlug(slug).get();
+ if (existingEvent) {
+ res.status(409).json({ error: 'Event with this slug already exists' });
+ return;
+ }
- const currentSeason = await db.seasons.getCurrent();
- if (!currentSeason) {
- res
- .status(400)
- .json({ error: 'No current season found. A season must be active to create an event.' });
- return;
- }
+ const currentSeason = await db.seasons.getCurrent();
+ if (!currentSeason) {
+ res
+ .status(400)
+ .json({ error: 'No current season found. A season must be active to create an event.' });
+ return;
+ }
- // Convert the selected date from 8:00 AM to 23:59 in the event's timezone to UTC range
- // Example: April 4 8:00 AM - 23:59 in Europe/Warsaw (UTC+2) becomes April 4 06:00 UTC to April 4 21:59 UTC
- let startDate: Date;
- let endDate: Date;
+ // Convert the selected date from 8:00 AM to 23:59 in the event's timezone to UTC range
+ // Example: April 4 8:00 AM - 23:59 in Europe/Warsaw (UTC+2) becomes April 4 06:00 UTC to April 4 21:59 UTC
+ let startDate: Date;
+ let endDate: Date;
- try {
- startDate = dayjs.tz(date, 'YYYY-MM-DD', timezone).hour(8).minute(0).second(0).toDate();
- endDate = dayjs.tz(date, 'YYYY-MM-DD', timezone).endOf('day').toDate();
- } catch {
- res.status(400).json({ error: 'Invalid date format or timezone' });
- return;
- }
+ try {
+ startDate = dayjs.tz(date, 'YYYY-MM-DD', timezone).hour(8).minute(0).second(0).toDate();
+ endDate = dayjs.tz(date, 'YYYY-MM-DD', timezone).endOf('day').toDate();
+ } catch {
+ res.status(400).json({ error: 'Invalid date format or timezone' });
+ return;
+ }
- const eventResult = await db.events.create({
- name,
- slug,
- start_date: startDate,
- end_date: endDate,
- location,
- region,
- timezone,
- season_id: currentSeason.id
- });
-
- if (!eventResult) {
- res.status(500).json({ error: 'Failed to create event' });
- return;
- }
+ const eventResult = await db.events.create({
+ name,
+ slug,
+ start_date: startDate,
+ end_date: endDate,
+ location,
+ region,
+ timezone,
+ season_id: currentSeason.id
+ });
+
+ if (!eventResult) {
+ res.status(500).json({ error: 'Failed to create event' });
+ return;
+ }
- await db.events.byId(eventResult.id).addAdmin(req.userId);
+ await db.events.byId(eventResult.id).addAdmin(req.userId);
- const divisionsResult = await db.divisions.createMany(
- divisions.map(division => ({
- name: division.name,
- color: division.color,
- event_id: eventResult.id
- }))
- );
+ const divisionsResult = await db.divisions.createMany(
+ divisions.map(division => ({
+ name: division.name,
+ color: division.color,
+ event_id: eventResult.id
+ }))
+ );
- if (!divisionsResult || divisionsResult.length === 0) {
- res.status(500).json({ error: 'Failed to create divisions' });
- return;
- }
+ if (!divisionsResult || divisionsResult.length === 0) {
+ res.status(500).json({ error: 'Failed to create divisions' });
+ return;
+ }
- res.status(201).json({ timezone });
- } catch (error) {
- console.error('Error creating event:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
+ res.status(201).json({ timezone });
+ } catch (error) {
+ console.error('Error creating event:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ })
+);
router.use('/:eventId', attachEvent());
@@ -161,82 +174,93 @@ router.use('/:eventId/users', eventUsersRouter);
router.use('/:eventId/settings', eventSettingsRouter);
router.use('/:eventId/integrations', eventIntegrationsRouter);
-router.get('/:eventId', async (req: AdminEventRequest, res) => {
- const event = await db.events.byId(req.eventId).get();
- res.json(makeAdminEventResponse(event));
-});
-
-router.put('/:eventId', requirePermission('MANAGE_EVENTS'), async (req: AdminRequest, res) => {
- try {
- const { eventId } = req.params;
- const { name, date, location, region } = req.body;
-
- if (!eventId || typeof eventId !== 'string') {
- res.status(400).json({ error: 'EVENT_ID_REQUIRED' });
- return;
- }
-
- const existingEvent = await db.events.byId(eventId).get();
- if (!existingEvent) {
+router.get(
+ '/:eventId',
+ asHandler(async (req, res) => {
+ const event = await db.events.byId(req.eventId).get();
+ if (!event) {
res.status(404).json({ error: 'Event not found' });
return;
}
+ res.json(makeAdminEventResponse(event));
+ })
+);
+
+router.put(
+ '/:eventId',
+ requirePermission('MANAGE_EVENTS'),
+ asHandler(async (req, res) => {
+ try {
+ const { eventId } = req.params;
+ const { name, date, location, region } = req.body;
- const updateData: Partial = {};
-
- if (name !== undefined) {
- if (!name.trim()) {
- res.status(400).json({ error: 'Name cannot be empty' });
+ if (!eventId || typeof eventId !== 'string') {
+ res.status(400).json({ error: 'EVENT_ID_REQUIRED' });
return;
}
- updateData.name = name;
- }
- if (date !== undefined) {
- const eventDate = new Date(date);
- if (isNaN(eventDate.getTime())) {
- res.status(400).json({ error: 'Invalid date format' });
+ const existingEvent = await db.events.byId(eventId).get();
+ if (!existingEvent) {
+ res.status(404).json({ error: 'Event not found' });
return;
}
- updateData.start_date = eventDate;
- updateData.end_date = eventDate; // For now, using same date for start and end
- }
- if (location !== undefined) {
- if (!location.trim()) {
- res.status(400).json({ error: 'Location cannot be empty' });
- return;
+ const updateData: Partial = {};
+
+ if (name !== undefined) {
+ if (!name.trim()) {
+ res.status(400).json({ error: 'Name cannot be empty' });
+ return;
+ }
+ updateData.name = name;
}
- updateData.location = location;
- }
- if (region !== undefined) {
- if (typeof region !== 'string' || region.length !== 2 || !/^[A-Z]{2}$/.test(region)) {
- res
- .status(400)
- .json({ error: 'Region must be a 2-letter ISO 3166-1 alpha-2 country code' });
+ if (date !== undefined) {
+ const eventDate = new Date(date);
+ if (isNaN(eventDate.getTime())) {
+ res.status(400).json({ error: 'Invalid date format' });
+ return;
+ }
+ updateData.start_date = eventDate;
+ updateData.end_date = eventDate; // For now, using same date for start and end
+ }
+
+ if (location !== undefined) {
+ if (!location.trim()) {
+ res.status(400).json({ error: 'Location cannot be empty' });
+ return;
+ }
+ updateData.location = location;
+ }
+
+ if (region !== undefined) {
+ if (typeof region !== 'string' || region.length !== 2 || !/^[A-Z]{2}$/.test(region)) {
+ res
+ .status(400)
+ .json({ error: 'Region must be a 2-letter ISO 3166-1 alpha-2 country code' });
+ return;
+ }
+ updateData.region = region;
+ }
+
+ if (Object.keys(updateData).length === 0) {
+ res.status(400).json({ error: 'No valid fields to update' });
return;
}
- updateData.region = region;
- }
- if (Object.keys(updateData).length === 0) {
- res.status(400).json({ error: 'No valid fields to update' });
- return;
- }
+ const updatedEvent = await db.events.byId(eventId).update(updateData);
- const updatedEvent = await db.events.byId(eventId).update(updateData);
+ if (!updatedEvent) {
+ res.status(500).json({ error: 'Failed to update event' });
+ return;
+ }
- if (!updatedEvent) {
- res.status(500).json({ error: 'Failed to update event' });
- return;
+ res.status(200).end();
+ } catch (error) {
+ console.error('Error updating event:', error);
+ res.status(500).json({ error: 'Internal server error' });
}
-
- res.status(200).end();
- } catch (error) {
- console.error('Error updating event:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/events/integrations/index.ts b/apps/backend/src/routers/admin/events/integrations/index.ts
index 4a478315f..51588d3cd 100644
--- a/apps/backend/src/routers/admin/events/integrations/index.ts
+++ b/apps/backend/src/routers/admin/events/integrations/index.ts
@@ -7,70 +7,78 @@ import {
import { AdminEventRequest } from '../../../../types/express';
import { requirePermission } from '../../middleware/require-permission';
import db from '../../../../lib/database';
+import { asHandler } from '../../../../types/express-handlers';
import { makeAdminIntegrationResponse, validateAndUpdateIntegration } from './util';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminEventRequest, res) => {
- try {
- const integrations = await db.integrations.byEventId(req.eventId).getAll();
- res.json(integrations.map(makeAdminIntegrationResponse));
- } catch (error) {
- console.error('Error fetching integrations:', error);
- res.status(500).json({ error: 'Failed to fetch integrations' });
- }
-});
-
-router.post('/', requirePermission('MANAGE_EVENT_DETAILS'), async (req: AdminEventRequest, res) => {
- try {
- const { type, settings, enabled } = req.body;
-
- if (!type) {
- res.status(400).json({ error: 'Integration type is required' });
- return;
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ try {
+ const integrations = await db.integrations.byEventId(req.eventId).getAll();
+ res.json(integrations.map(makeAdminIntegrationResponse));
+ } catch (error) {
+ console.error('Error fetching integrations:', error);
+ res.status(500).json({ error: 'Failed to fetch integrations' });
}
+ })
+);
- // Validate the integration type exists
- getIntegrationConfig(type);
+router.post(
+ '/',
+ requirePermission('MANAGE_EVENT_DETAILS'),
+ asHandler(async (req, res) => {
+ try {
+ const { type, settings, enabled } = req.body;
- const validatedSettings = validateIntegrationSettings(type, settings || {});
+ if (!type) {
+ res.status(400).json({ error: 'Integration type is required' });
+ return;
+ }
- const existing = await db.integrations.byType(req.eventId, type).get();
- if (existing) {
- res
- .status(409)
- .json({ error: `Integration of type "${type}" already exists for this event` });
- return;
- }
+ // Validate the integration type exists
+ getIntegrationConfig(type);
- const integration = await db.integrations.create({
- event_id: req.eventId,
- integration_type: type,
- enabled: enabled !== false,
- settings: validatedSettings
- });
-
- res.status(201).json(makeAdminIntegrationResponse(integration));
- } catch (error) {
- if (error instanceof Error && error.message.includes('Unknown integration type')) {
- res.status(400).json({ error: error.message });
- return;
- }
+ const validatedSettings = validateIntegrationSettings(type, settings || {});
- if (error.message.includes('validation')) {
- res.status(400).json({ error: `Invalid settings: ${error.message}` });
- return;
- }
+ const existing = await db.integrations.byType(req.eventId, type).get();
+ if (existing) {
+ res
+ .status(409)
+ .json({ error: `Integration of type "${type}" already exists for this event` });
+ return;
+ }
+
+ const integration = await db.integrations.create({
+ event_id: req.eventId,
+ integration_type: type,
+ enabled: enabled !== false,
+ settings: validatedSettings
+ });
+
+ res.status(201).json(makeAdminIntegrationResponse(integration));
+ } catch (error) {
+ if (error instanceof Error && error.message.includes('Unknown integration type')) {
+ res.status(400).json({ error: error.message });
+ return;
+ }
- console.error('Error creating integration:', error);
- res.status(500).json({ error: 'Failed to create integration' });
- }
-});
+ if (error instanceof Error && error.message.includes('validation')) {
+ res.status(400).json({ error: `Invalid settings: ${error.message}` });
+ return;
+ }
+
+ console.error('Error creating integration:', error);
+ res.status(500).json({ error: 'Failed to create integration' });
+ }
+ })
+);
router.put(
'/:id',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { id: integrationId } = req.params;
if (!integrationId || typeof integrationId !== 'string') {
res.status(400).json({ error: 'INTEGRATION_ID_REQUIRED' });
@@ -124,13 +132,13 @@ router.put(
console.error('Error updating integration:', error);
res.status(500).json({ error: 'Failed to update integration' });
}
- }
+ })
);
router.delete(
'/:id',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { id: integrationId } = req.params;
if (!integrationId || typeof integrationId !== 'string') {
res.status(400).json({ error: 'INTEGRATION_ID_REQUIRED' });
@@ -156,7 +164,7 @@ router.delete(
console.error('Error deleting integration:', error);
res.status(500).json({ error: 'Failed to delete integration' });
}
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/settings/index.ts b/apps/backend/src/routers/admin/events/settings/index.ts
index f3f4de199..d903b58bb 100644
--- a/apps/backend/src/routers/admin/events/settings/index.ts
+++ b/apps/backend/src/routers/admin/events/settings/index.ts
@@ -11,6 +11,7 @@ import { publishEventResults } from '../../../integrations/sendgrid/publish';
import { generateEventResultsZip } from '../../../../lib/results-download';
import { createSseEmitter } from '../../../../lib/sse';
import { storeTempFile, consumeTempFile } from '../../../../lib/temp-download-store';
+import { asHandler } from '../../../../types/express-handlers';
import { makeAdminSettingsResponse, makeUpdateableEventSettings } from './util';
const router = express.Router({ mergeParams: true });
@@ -19,214 +20,232 @@ const downloadRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
standardHeaders: true,
- legacyHeaders: false,
+ legacyHeaders: false
});
const downloadFileRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 30,
standardHeaders: true,
- legacyHeaders: false,
+ legacyHeaders: false
});
-router.get('/', async (req: AdminEventRequest, res) => {
- const settings = await db.events.byId(req.eventId).getSettings();
- if (!settings) {
- res.status(404).json({ error: 'Event not found' });
- return;
- }
-
- res.json(makeAdminSettingsResponse(settings));
-});
-
-router.put('/', async (req: AdminEventRequest, res) => {
- const updateData = makeUpdateableEventSettings(req.body);
- const updatedSettings = await db.events.byId(req.eventId).updateSettings(updateData);
- if (!updatedSettings) {
- throw new Error('Failed to update event settings');
- }
- res.json({ success: true });
-});
-
-router.post('/complete', async (req: AdminEventRequest, res) => {
- const updatedSettings = await db.events.byId(req.eventId).updateSettings({ completed: true });
- if (!updatedSettings) {
- throw new Error('Failed to complete event');
- }
- res.json({ success: true });
-});
-
-router.post('/publish', async (req: AdminEventRequest, res) => {
- const emitter = createSseEmitter(res);
-
- try {
+router.get(
+ '/',
+ asHandler(async (req, res) => {
const settings = await db.events.byId(req.eventId).getSettings();
if (!settings) {
- emitter.sendFailure('Event not found');
- return;
- }
-
- if (!settings.completed) {
- emitter.sendFailure('Event must be completed before publishing');
+ res.status(404).json({ error: 'Event not found' });
return;
}
- emitter.sendStart();
+ res.json(makeAdminSettingsResponse(settings));
+ })
+);
- const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true });
+router.put(
+ '/',
+ asHandler(async (req, res) => {
+ const updateData = makeUpdateableEventSettings(req.body);
+ const updatedSettings = await db.events.byId(req.eventId).updateSettings(updateData);
if (!updatedSettings) {
- throw new Error('Failed to publish event');
+ throw new Error('Failed to update event settings');
}
+ res.json({ success: true });
+ })
+);
+
+router.post(
+ '/complete',
+ asHandler(async (req, res) => {
+ const updatedSettings = await db.events.byId(req.eventId).updateSettings({ completed: true });
+ if (!updatedSettings) {
+ throw new Error('Failed to complete event');
+ }
+ res.json({ success: true });
+ })
+);
+
+router.post(
+ '/publish',
+ asHandler(async (req, res) => {
+ const emitter = createSseEmitter(res);
+
+ try {
+ const settings = await db.events.byId(req.eventId).getSettings();
+ if (!settings) {
+ emitter.sendFailure('Event not found');
+ return;
+ }
- // Send emails if SendGrid integration is enabled
- const integrations = await db.integrations.byEventId(req.eventId).getAll();
- const sendgridIntegration = integrations.find(
- i => i.integration_type === IntegrationTypes.SENDGRID && i.enabled
- );
-
- if (sendgridIntegration) {
- try {
- const emailResult = await publishEventResults({
- eventId: req.eventId,
- settings: sendgridIntegration.settings,
- onProgress: (percent, message) => emitter.sendProgress(percent, message)
- });
- console.info(
- `Event ${req.eventId} published with emails sent. ` +
- `Total: ${emailResult.total}, Failed: ${emailResult.failed}`
- );
+ if (!settings.completed) {
+ emitter.sendFailure('Event must be completed before publishing');
+ return;
+ }
+
+ emitter.sendStart();
+
+ const updatedSettings = await db.events.byId(req.eventId).updateSettings({ published: true });
+ if (!updatedSettings) {
+ throw new Error('Failed to publish event');
+ }
+
+ // Send emails if SendGrid integration is enabled
+ const integrations = await db.integrations.byEventId(req.eventId).getAll();
+ const sendgridIntegration = integrations.find(
+ i => i.integration_type === IntegrationTypes.SENDGRID && i.enabled
+ );
+
+ if (sendgridIntegration) {
+ try {
+ const emailResult = await publishEventResults({
+ eventId: req.eventId,
+ settings: sendgridIntegration.settings,
+ onProgress: (percent, message) => emitter.sendProgress(percent, message)
+ });
+ console.info(
+ `Event ${req.eventId} published with emails sent. ` +
+ `Total: ${emailResult.total}, Failed: ${emailResult.failed}`
+ );
+ emitter.sendSuccess({
+ published: true,
+ emailsSent: emailResult.total,
+ emailsFailed: emailResult.failed,
+ failedEmails: emailResult.failedEmails
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error('Error sending SendGrid emails:', error);
+ emitter.sendFailure(`Failed to send emails: ${message}`);
+ }
+ } else {
+ console.info(`Event ${req.eventId} published`);
emitter.sendSuccess({
published: true,
- emailsSent: emailResult.total,
- emailsFailed: emailResult.failed,
- failedEmails: emailResult.failedEmails
+ emailsSent: 0,
+ emailsFailed: 0
});
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- console.error('Error sending SendGrid emails:', error);
- emitter.sendFailure(`Failed to send emails: ${message}`);
}
- } else {
- console.info(`Event ${req.eventId} published`);
- emitter.sendSuccess({
- published: true,
- emailsSent: 0,
- emailsFailed: 0
- });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`Error publishing event ${req.eventId}: ${message}`, error);
+ emitter.sendFailure(`Failed to publish event: ${message}`);
}
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- console.error(`Error publishing event ${req.eventId}: ${message}`, error);
- emitter.sendFailure(`Failed to publish event: ${message}`);
- }
-});
+ })
+);
+
+router.post(
+ '/download',
+ downloadRateLimiter,
+ asHandler(async (req, res) => {
+ const emitter = createSseEmitter(res);
+ let tempPath: string | undefined;
+ let fileStream: fs.WriteStream | undefined;
+ let archive:
+ | (NodeJS.ReadableStream & {
+ on(event: 'error', listener: (error: Error) => void): unknown;
+ pipe(destination: T): T;
+ finalize(): unknown;
+ destroy?: (error?: Error) => void;
+ })
+ | undefined;
+
+ try {
+ const settings = await db.events.byId(req.eventId).getSettings();
+ if (!settings) {
+ emitter.sendFailure('Event not found');
+ return;
+ }
-router.post('/download', downloadRateLimiter, async (req: AdminEventRequest, res) => {
- const emitter = createSseEmitter(res);
- let tempPath: string | undefined;
- let fileStream: fs.WriteStream | undefined;
- let archive:
- | (NodeJS.ReadableStream & {
- on(event: 'error', listener: (error: Error) => void): unknown;
- pipe(destination: T): T;
- finalize(): unknown;
- destroy?: (error?: Error) => void;
- })
- | undefined;
-
- try {
- const settings = await db.events.byId(req.eventId).getSettings();
- if (!settings) {
- emitter.sendFailure('Event not found');
- return;
- }
+ if (!settings.published) {
+ emitter.sendFailure('Event must be published before downloading results');
+ return;
+ }
- if (!settings.published) {
- emitter.sendFailure('Event must be published before downloading results');
- return;
- }
+ const language = (req.query.language as string) || 'en';
+ console.info(
+ `Starting results ZIP generation for event ${req.eventId} (language: ${language})`
+ );
- const language = (req.query.language as string) || 'en';
- console.info(
- `Starting results ZIP generation for event ${req.eventId} (language: ${language})`
- );
-
- emitter.sendStart();
-
- const result = await generateEventResultsZip(
- req.eventId,
- language,
- percent => emitter.sendProgress(percent)
- );
- archive = result.archive;
- const { fileName, statistics } = result;
-
- if (statistics.teamsWithPdfs === 0) {
- console.error(
- `CRITICAL: Event ${req.eventId} generated an empty ZIP with 0 successful PDFs. ` +
- `Total teams: ${statistics.totalTeams}, Failed PDFs: ${statistics.failedPdfs}.`
+ emitter.sendStart();
+
+ const result = await generateEventResultsZip(req.eventId, language, percent =>
+ emitter.sendProgress(percent)
);
- emitter.sendFailure('Failed to generate results: no PDFs were created.');
- return;
- }
+ archive = result.archive;
+ const { fileName, statistics } = result;
- console.info(
- `Results ZIP ready: ${fileName} ` +
- `(${statistics.teamsWithPdfs}/${statistics.totalTeams} teams with PDFs, ${statistics.failedPdfs} failed PDFs)`
- );
+ if (statistics.teamsWithPdfs === 0) {
+ console.error(
+ `CRITICAL: Event ${req.eventId} generated an empty ZIP with 0 successful PDFs. ` +
+ `Total teams: ${statistics.totalTeams}, Failed PDFs: ${statistics.failedPdfs}.`
+ );
+ emitter.sendFailure('Failed to generate results: no PDFs were created.');
+ return;
+ }
- tempPath = path.join(os.tmpdir(), `${crypto.randomUUID()}.zip`);
- fileStream = fs.createWriteStream(tempPath);
+ console.info(
+ `Results ZIP ready: ${fileName} ` +
+ `(${statistics.teamsWithPdfs}/${statistics.totalTeams} teams with PDFs, ${statistics.failedPdfs} failed PDFs)`
+ );
- await new Promise((resolve, reject) => {
- fileStream!.on('close', resolve);
- fileStream!.on('error', reject);
- archive!.on('error', reject);
- archive!.pipe(fileStream!);
- archive!.finalize();
- });
+ tempPath = path.join(os.tmpdir(), `${crypto.randomUUID()}.zip`);
+ fileStream = fs.createWriteStream(tempPath);
- const token = storeTempFile(tempPath, fileName);
- emitter.sendSuccess({ token });
- } catch (error) {
- archive?.destroy?.(error instanceof Error ? error : undefined);
- fileStream?.destroy(error instanceof Error ? error : undefined);
-
- if (tempPath) {
- try {
- await fs.promises.unlink(tempPath);
- } catch (unlinkError) {
- if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') {
- console.error(
- `Failed to clean up temporary results ZIP for event ${req.eventId}: ${tempPath}`,
- unlinkError
- );
+ await new Promise((resolve, reject) => {
+ fileStream!.on('close', resolve);
+ fileStream!.on('error', reject);
+ archive!.on('error', reject);
+ archive!.pipe(fileStream!);
+ archive!.finalize();
+ });
+
+ const token = storeTempFile(tempPath, fileName);
+ emitter.sendSuccess({ token });
+ } catch (error) {
+ archive?.destroy?.(error instanceof Error ? error : undefined);
+ fileStream?.destroy(error instanceof Error ? error : undefined);
+
+ if (tempPath) {
+ try {
+ await fs.promises.unlink(tempPath);
+ } catch (unlinkError) {
+ if ((unlinkError as NodeJS.ErrnoException).code !== 'ENOENT') {
+ console.error(
+ `Failed to clean up temporary results ZIP for event ${req.eventId}: ${tempPath}`,
+ unlinkError
+ );
+ }
}
}
+
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`Error downloading event results for event ${req.eventId}: ${message}`, error);
+ emitter.sendFailure(`Failed to download event results: ${message}`);
+ }
+ })
+);
+
+router.get(
+ '/download/file',
+ downloadFileRateLimiter,
+ asHandler((req, res) => {
+ const token = req.query.token as string;
+ if (!token) {
+ res.status(400).json({ error: 'Missing token' });
+ return;
}
- const message = error instanceof Error ? error.message : String(error);
- console.error(`Error downloading event results for event ${req.eventId}: ${message}`, error);
- emitter.sendFailure(`Failed to download event results: ${message}`);
- }
-});
+ const entry = consumeTempFile(token);
+ if (!entry) {
+ res.status(404).json({ error: 'Token not found or expired' });
+ return;
+ }
-router.get('/download/file', downloadFileRateLimiter, (req: AdminEventRequest, res) => {
- const token = req.query.token as string;
- if (!token) {
- res.status(400).json({ error: 'Missing token' });
- return;
- }
-
- const entry = consumeTempFile(token);
- if (!entry) {
- res.status(404).json({ error: 'Token not found or expired' });
- return;
- }
-
- res.download(entry.filePath, entry.fileName, () => {
- fs.unlink(entry.filePath, () => {});
- });
-});
+ res.download(entry.filePath, entry.fileName, () => {
+ fs.unlink(entry.filePath, () => {});
+ });
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/events/teams/index.ts b/apps/backend/src/routers/admin/events/teams/index.ts
index a0591060a..ecc392dcf 100644
--- a/apps/backend/src/routers/admin/events/teams/index.ts
+++ b/apps/backend/src/routers/admin/events/teams/index.ts
@@ -4,24 +4,31 @@ import db from '../../../../lib/database';
import { requirePermission } from '../../middleware/require-permission';
import { AdminEventRequest } from '../../../../types/express';
import { makeAdminTeamResponse, makeAdminTeamWithDivisionResponse } from '../../teams/util';
+import { asHandler } from '../../../../types/express-handlers';
import { isTeamsRegistration, parseTeamCSVRegistration } from './utils';
const router = express.Router({ mergeParams: true });
-router.get('/', async (req: AdminEventRequest, res) => {
- const teams = await db.events.byId(req.eventId).getRegisteredTeams();
- res.json(teams.map(team => makeAdminTeamWithDivisionResponse(team)));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const teams = await db.events.byId(req.eventId).getRegisteredTeams();
+ res.json(teams.map(team => makeAdminTeamWithDivisionResponse(team)));
+ })
+);
-router.get('/available', async (req: AdminEventRequest, res) => {
- const teams = await db.events.byId(req.eventId).getAvailableTeams();
- res.json(teams.map(team => makeAdminTeamResponse(team)));
-});
+router.get(
+ '/available',
+ asHandler(async (req, res) => {
+ const teams = await db.events.byId(req.eventId).getAvailableTeams();
+ res.json(teams.map(team => makeAdminTeamResponse(team)));
+ })
+);
router.post(
'/register',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const registration = req.body;
if (!registration || !isTeamsRegistration(registration)) {
@@ -32,13 +39,13 @@ router.post(
await db.events.byId(req.eventId).registerTeams(registration);
res.status(200).end();
- }
+ })
);
router.delete(
'/remove',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const teamsToRemove = req.body;
if (!teamsToRemove || !Array.isArray(teamsToRemove)) {
@@ -49,13 +56,13 @@ router.delete(
await db.events.byId(req.eventId).removeTeams(teamsToRemove);
res.status(200).end();
- }
+ })
);
router.put(
'/:teamId/division',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { teamId } = req.params;
const { divisionId } = req.body;
@@ -76,13 +83,13 @@ router.put(
console.error('Error changing team division:', error);
res.status(500).json({ error: 'Failed to change team division' });
}
- }
+ })
);
router.post(
'/register-from-csv',
[requirePermission('MANAGE_EVENT_DETAILS'), fileUpload()],
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
if (!req.files || !req.files.file) {
res.status(400).json({ error: 'No file uploaded' });
return;
@@ -145,7 +152,7 @@ router.post(
// TODO: Split DB operations into another function
// console.error('Error registering teams from CSV:', error);
// res.status(500).json({ error: 'Failed to register teams from CSV' });
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/users/index.ts b/apps/backend/src/routers/admin/events/users/index.ts
index c7df3e472..bed691601 100644
--- a/apps/backend/src/routers/admin/events/users/index.ts
+++ b/apps/backend/src/routers/admin/events/users/index.ts
@@ -3,6 +3,7 @@ import db from '../../../../lib/database';
import { AdminEventRequest } from '../../../../types/express';
import { makeAdminUserResponse } from '../../users/util';
import { requirePermission } from '../../middleware/require-permission';
+import { asHandler } from '../../../../types/express-handlers';
import {
makeAdminVolunteerResponse,
generateVolunteerPassword,
@@ -12,25 +13,28 @@ import {
const router = express.Router({ mergeParams: true });
-router.get('/admins', async (req: AdminEventRequest, res) => {
- const admins = await db.admins.byEventId(req.eventId).getAll();
- res.json(admins.map(admin => makeAdminUserResponse(admin)));
-});
+router.get(
+ '/admins',
+ asHandler(async (req, res) => {
+ const admins = await db.admins.byEventId(req.eventId).getAll();
+ res.json(admins.map(admin => makeAdminUserResponse(admin)));
+ })
+);
router.post(
'/admins',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { adminIds } = req.body;
- adminIds.forEach(async id => await db.events.byId(req.eventId).addAdmin(id));
+ adminIds.forEach(async (id: string) => await db.events.byId(req.eventId).addAdmin(id));
res.status(204).end();
- }
+ })
);
router.delete(
'/admins/:adminId',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { adminId } = req.params;
if (!adminId || typeof adminId !== 'string') {
res.status(400).json({ error: 'Admin ID is required' });
@@ -43,18 +47,21 @@ router.delete(
}
await db.events.byId(req.eventId).removeAdmin(adminId);
res.status(204).end();
- }
+ })
);
-router.get('/volunteers', async (req: AdminEventRequest, res) => {
- const volunteers = await db.eventUsers.byEventId(req.eventId).getAll();
- res.json(volunteers.map(volunteer => makeAdminVolunteerResponse(volunteer)));
-});
+router.get(
+ '/volunteers',
+ asHandler(async (req, res) => {
+ const volunteers = await db.eventUsers.byEventId(req.eventId).getAll();
+ res.json(volunteers.map(volunteer => makeAdminVolunteerResponse(volunteer)));
+ })
+);
router.post(
'/volunteers',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const { volunteers } = req.body;
if (!Array.isArray(volunteers) || volunteers.length === 0) {
@@ -108,13 +115,13 @@ router.post(
res
.status(201)
.json(finalVolunteersWithDivisions.map(volunteer => makeAdminVolunteerResponse(volunteer)));
- }
+ })
);
router.get(
'/volunteers/passwords',
requirePermission('MANAGE_EVENT_DETAILS'),
- async (req: AdminEventRequest, res) => {
+ asHandler(async (req, res) => {
const volunteers = await db.eventUsers.byEventId(req.eventId).getAll();
const divisions = await db.divisions.byEventId(req.eventId).getAll();
@@ -136,7 +143,7 @@ router.get(
);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.send(csvContent);
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/events/users/util.ts b/apps/backend/src/routers/admin/events/users/util.ts
index 38d93c479..3be2a0b81 100644
--- a/apps/backend/src/routers/admin/events/users/util.ts
+++ b/apps/backend/src/routers/admin/events/users/util.ts
@@ -36,45 +36,59 @@ export const makeAdminVolunteerResponse = (
divisions: user.divisions
});
-export const getRoleInfoMapping = async (divisions: Division[]): Promise> => {
+export const getRoleInfoMapping = async (
+ divisions: Division[]
+): Promise> => {
const roleInfoMapping: Record = {};
-
+
divisions.forEach(division => {
roleInfoMapping[division.id] = division.name;
});
- await Promise.all(divisions.map(async division => {
- const [rooms, tables] = await Promise.all([
- db.rooms.byDivisionId(division.id).getAll(),
- db.tables.byDivisionId(division.id).getAll()
- ]);
+ await Promise.all(
+ divisions.map(async division => {
+ const [rooms, tables] = await Promise.all([
+ db.rooms.byDivisionId(division.id).getAll(),
+ db.tables.byDivisionId(division.id).getAll()
+ ]);
- rooms.forEach(room => {
- roleInfoMapping[room.id] = room.name;
- });
+ rooms.forEach(room => {
+ roleInfoMapping[room.id] = room.name;
+ });
- tables.forEach(table => {
- roleInfoMapping[table.id] = table.name;
- });
- }));
+ tables.forEach(table => {
+ roleInfoMapping[table.id] = table.name;
+ });
+ })
+ );
return roleInfoMapping;
-}
+};
-export const getDivisionNamesString = (divisionIds: string[], infoMapping: Record) : string => {
+export const getDivisionNamesString = (
+ divisionIds: string[],
+ infoMapping: Record
+): string => {
return divisionIds.map(id => infoMapping[id] || id).join('; ');
-}
+};
-const parseRoleInfo = (roleInfo: Record, infoMapping: Record): string => {
- if (!roleInfo) return null;
- Object.entries(roleInfo).map(([key, value]) => {
+const parseRoleInfo = (
+ roleInfo: Record | null,
+ infoMapping: Record
+): string => {
+ if (!roleInfo) return '';
+ const roleInfoCopy = { ...roleInfo };
+ Object.entries(roleInfoCopy).forEach(([key, value]) => {
if (typeof value === 'string' && infoMapping[value]) {
- roleInfo[key] = infoMapping[value];
+ roleInfoCopy[key] = infoMapping[value];
}
});
- return JSON.stringify(roleInfo);
-}
+ return JSON.stringify(roleInfoCopy);
+};
-export const formatVolunteerInfo = (user: EventUser & { divisions: string[] }, infoMapping: Record) : string => {
+export const formatVolunteerInfo = (
+ user: EventUser & { divisions: string[] },
+ infoMapping: Record
+): string => {
return `${user.role || ''},${getDivisionNamesString(user.divisions, infoMapping)},${user.identifier || ''},${parseRoleInfo(user.role_info, infoMapping)},${user.password}`;
-}
+};
diff --git a/apps/backend/src/routers/admin/middleware/attach-division.ts b/apps/backend/src/routers/admin/middleware/attach-division.ts
index 9645d0738..a112f5f53 100644
--- a/apps/backend/src/routers/admin/middleware/attach-division.ts
+++ b/apps/backend/src/routers/admin/middleware/attach-division.ts
@@ -1,6 +1,6 @@
-import { NextFunction, Response } from 'express';
import { AdminDivisionRequest, AdminEventRequest } from '../../../types/express';
import database from '../../../lib/database';
+import { asMiddleware } from '../../../types/express-handlers';
/**
* Middleware to attach the division ID to the request.
@@ -8,7 +8,7 @@ import database from '../../../lib/database';
* If the division is not bound to the event preceding it, a 400 error is returned.
*/
export const attachDivision = () => {
- return async (req: AdminEventRequest, res: Response, next: NextFunction) => {
+ return asMiddleware(async (req, res, next) => {
try {
const divisionId = req.params.divisionId;
@@ -36,5 +36,5 @@ export const attachDivision = () => {
console.error('Error attaching division:', error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- };
+ });
};
diff --git a/apps/backend/src/routers/admin/middleware/attach-event.ts b/apps/backend/src/routers/admin/middleware/attach-event.ts
index 02a3586bf..352951728 100644
--- a/apps/backend/src/routers/admin/middleware/attach-event.ts
+++ b/apps/backend/src/routers/admin/middleware/attach-event.ts
@@ -1,6 +1,6 @@
-import { NextFunction, Response } from 'express';
import { AdminEventRequest, AdminRequest } from '../../../types/express';
import database from '../../../lib/database';
+import { asMiddleware } from '../../../types/express-handlers';
/**
* Middleware to attach the event ID to the request.
@@ -8,7 +8,7 @@ import database from '../../../lib/database';
* If the user tries to access an event they are not assigned to, a 403 error is returned.
*/
export const attachEvent = () => {
- return async (req: AdminRequest, res: Response, next: NextFunction) => {
+ return asMiddleware(async (req, res, next) => {
try {
const eventId = req.params.eventId;
if (!eventId || typeof eventId !== 'string') {
@@ -37,5 +37,5 @@ export const attachEvent = () => {
console.error('Error attaching event:', error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- };
+ });
};
diff --git a/apps/backend/src/routers/admin/middleware/auth.ts b/apps/backend/src/routers/admin/middleware/auth.ts
index 3f074b99d..a6ee6e9d8 100644
--- a/apps/backend/src/routers/admin/middleware/auth.ts
+++ b/apps/backend/src/routers/admin/middleware/auth.ts
@@ -6,6 +6,10 @@ import { extractToken } from '../../../lib/security/auth';
const jwtSecret = process.env.JWT_SECRET;
+if (!jwtSecret) {
+ throw new Error('JWT_SECRET environment variable is required');
+}
+
const publicPaths = new Set(['/auth/login', '/auth/logout']);
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
@@ -15,7 +19,7 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction)
try {
const token = extractToken(req, 'admin-auth-token');
- const tokenData = jwt.verify(token, jwtSecret) as JwtTokenData;
+ const tokenData = jwt.verify(token, jwtSecret) as unknown as JwtTokenData;
if (tokenData.userType !== 'admin') {
res.clearCookie('admin-auth-token');
@@ -23,7 +27,7 @@ export const authMiddleware = (req: Request, res: Response, next: NextFunction)
return;
}
- if (tokenData.exp > Date.now() / 1000) {
+ if (tokenData.exp && tokenData.exp > Date.now() / 1000) {
const adminReq = req as AdminRequest;
adminReq.userId = tokenData.userId;
adminReq.userType = tokenData.userType;
diff --git a/apps/backend/src/routers/admin/middleware/require-event-assignment.ts b/apps/backend/src/routers/admin/middleware/require-event-assignment.ts
index 563eb8742..abec0c1de 100644
--- a/apps/backend/src/routers/admin/middleware/require-event-assignment.ts
+++ b/apps/backend/src/routers/admin/middleware/require-event-assignment.ts
@@ -1,6 +1,6 @@
-import { NextFunction, Response } from 'express';
import { AdminRequest } from '../../../types/express';
import database from '../../../lib/database';
+import { asMiddleware } from '../../../types/express-handlers';
/**
* Middleware factory that creates a middleware to check if the authenticated admin
@@ -14,7 +14,7 @@ export const requireEventAssignment = (
eventIdentifier: string,
identifierType: 'id' | 'slug' = 'id'
) => {
- return async (req: AdminRequest, res: Response, next: NextFunction) => {
+ return asMiddleware(async (req, res, next) => {
try {
let eventId: string;
@@ -41,5 +41,5 @@ export const requireEventAssignment = (
console.error('Error checking event assignment:', error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- };
+ });
};
diff --git a/apps/backend/src/routers/admin/middleware/require-permission.ts b/apps/backend/src/routers/admin/middleware/require-permission.ts
index 54859d1eb..f1ab42196 100644
--- a/apps/backend/src/routers/admin/middleware/require-permission.ts
+++ b/apps/backend/src/routers/admin/middleware/require-permission.ts
@@ -1,7 +1,7 @@
-import { NextFunction, Response } from 'express';
import { PermissionType } from '@lems/database';
import { AdminRequest } from '../../../types/express';
import db from '../../../lib/database';
+import { asMiddleware } from '../../../types/express-handlers';
/**
* Middleware factory that creates a middleware to check if the authenticated admin
@@ -11,7 +11,7 @@ import db from '../../../lib/database';
* @returns Express middleware function
*/
export const requirePermission = (permission: PermissionType) => {
- return async (req: AdminRequest, res: Response, next: NextFunction) => {
+ return asMiddleware(async (req, res, next) => {
try {
const hasPermission = await db.admins.byId(req.userId).hasPermission(permission);
@@ -25,5 +25,5 @@ export const requirePermission = (permission: PermissionType) => {
console.error('Error checking permission:', error);
res.status(500).json({ error: 'INTERNAL_SERVER_ERROR' });
}
- };
+ });
};
diff --git a/apps/backend/src/routers/admin/seasons/index.ts b/apps/backend/src/routers/admin/seasons/index.ts
index cece2a6f9..009163858 100644
--- a/apps/backend/src/routers/admin/seasons/index.ts
+++ b/apps/backend/src/routers/admin/seasons/index.ts
@@ -3,6 +3,7 @@ import fileUpload from 'express-fileupload';
import db from '../../../lib/database';
import { requirePermission } from '../middleware/require-permission';
import { AdminRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import { makeAdminSeasonResponse } from './util';
const router = express.Router({ mergeParams: true });
@@ -11,7 +12,7 @@ router.post(
'/',
requirePermission('MANAGE_SEASONS'),
fileUpload(),
- async (req: AdminRequest, res) => {
+ asHandler(async (req, res) => {
const { slug, name, startDate, endDate } = req.body;
if (!slug || !name || !startDate || !endDate) {
@@ -49,54 +50,66 @@ router.post(
}
res.status(201).json(makeAdminSeasonResponse(season));
- }
+ })
);
-router.get('/', async (req: AdminRequest, res) => {
- const seasons = await db.seasons.getAll();
- res.status(200).json(seasons.map(makeAdminSeasonResponse));
-});
-
-router.get('/current', async (req: AdminRequest, res) => {
- const currentSeason = await db.seasons.getCurrent();
- if (!currentSeason) {
- res.status(404).json({ error: 'No current season found' });
- return;
- }
- res.status(200).json(makeAdminSeasonResponse(currentSeason));
-});
-
-router.get('/id/:id', async (req: AdminRequest, res) => {
- const { id } = req.params;
- if (!id || typeof id !== 'string') {
- res.status(400).json({ error: 'Invalid season ID' });
- return;
- }
-
- const season = await db.seasons.byId(id).get();
- if (!season) {
- res.status(404).json({ error: 'Season not found' });
- return;
- }
- res.status(200).json(makeAdminSeasonResponse(season));
-});
-
-router.get('/:slug', async (req: AdminRequest, res) => {
- const { slug } = req.params;
-
- if (!slug || typeof slug !== 'string') {
- res.status(400).json({ error: 'Invalid season slug' });
- return;
- }
-
- const season = await db.seasons.bySlug(slug).get();
-
- if (!season) {
- res.status(404).json({ error: 'Season not found' });
- return;
- }
-
- res.status(200).json(makeAdminSeasonResponse(season));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const seasons = await db.seasons.getAll();
+ res.status(200).json(seasons.map(makeAdminSeasonResponse));
+ })
+);
+
+router.get(
+ '/current',
+ asHandler(async (req, res) => {
+ const currentSeason = await db.seasons.getCurrent();
+ if (!currentSeason) {
+ res.status(404).json({ error: 'No current season found' });
+ return;
+ }
+ res.status(200).json(makeAdminSeasonResponse(currentSeason));
+ })
+);
+
+router.get(
+ '/id/:id',
+ asHandler(async (req, res) => {
+ const { id } = req.params;
+ if (!id || typeof id !== 'string') {
+ res.status(400).json({ error: 'Invalid season ID' });
+ return;
+ }
+
+ const season = await db.seasons.byId(id).get();
+ if (!season) {
+ res.status(404).json({ error: 'Season not found' });
+ return;
+ }
+ res.status(200).json(makeAdminSeasonResponse(season));
+ })
+);
+
+router.get(
+ '/:slug',
+ asHandler(async (req, res) => {
+ const { slug } = req.params;
+
+ if (!slug || typeof slug !== 'string') {
+ res.status(400).json({ error: 'Invalid season slug' });
+ return;
+ }
+
+ const season = await db.seasons.bySlug(slug).get();
+
+ if (!season) {
+ res.status(404).json({ error: 'Season not found' });
+ return;
+ }
+
+ res.status(200).json(makeAdminSeasonResponse(season));
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/teams/index.ts b/apps/backend/src/routers/admin/teams/index.ts
index bb2230e4e..8713c27a0 100644
--- a/apps/backend/src/routers/admin/teams/index.ts
+++ b/apps/backend/src/routers/admin/teams/index.ts
@@ -4,81 +4,92 @@ import { ensureArray } from '@lems/shared/utils';
import db from '../../../lib/database';
import { AdminRequest } from '../../../types/express';
import { requirePermission } from '../middleware/require-permission';
+import { asHandler } from '../../../types/express-handlers';
import { makeAdminTeamResponse, parseTeamList } from './util';
const router = express.Router({ mergeParams: true });
const FILE_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
-router.get('/', async (req: AdminRequest, res) => {
- let teams = await db.teams.getAllWithActiveStatus();
-
- const extraFields = ensureArray(req.query.extraFields).map(field => field.toLowerCase());
-
- if (extraFields.includes('deletable')) {
- const unregistered = await db.teams.getAllUnregistered();
- const unregisteredIds = new Set(unregistered.map(t => t.id));
-
- teams = teams.map(team => {
- return {
- ...team,
- deletable: unregisteredIds.has(team.id)
- };
- });
- }
-
- const response = teams.map(team => makeAdminTeamResponse(team));
- res.json(response);
-});
-
-router.delete('/:teamId', requirePermission('MANAGE_TEAMS'), async (req: AdminRequest, res) => {
- const teamId = req.params.teamId;
- if (!teamId || typeof teamId !== 'string') {
- res.status(400).json({ error: 'Team ID is required' });
- return;
- }
-
- const team = await db.teams.byId(teamId).get();
- if (!team) {
- res.status(404).json({ error: 'Team not found' });
- return;
- }
-
- const teamEvents = await db.events.byTeam(team.id).getAll();
-
- if (teamEvents.length > 0) {
- res.status(400).json({ error: 'Cannot delete team that is registered for an event' });
- return;
- }
-
- const success = await db.teams.byId(teamId).delete();
- if (!success) {
- res.status(500).json({ error: 'Could not delete team' });
- return;
- }
-
- res.status(200).end();
-});
-
-router.get('/:teamId', async (req: AdminRequest, res) => {
- const id = req.params.teamId;
- if (!id || typeof id !== 'string') {
- res.status(400).json({ error: 'Team ID is required' });
- return;
- }
-
- const team = await db.teams.byId(id).get();
- if (!team) {
- res.status(404).json({ error: 'Team not found' });
- return;
- }
- res.json(makeAdminTeamResponse(team));
-});
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ let teams = await db.teams.getAllWithActiveStatus();
+
+ const extraFields = ensureArray(req.query.extraFields).map(field => field.toLowerCase());
+
+ if (extraFields.includes('deletable')) {
+ const unregistered = await db.teams.getAllUnregistered();
+ const unregisteredIds = new Set(unregistered.map(t => t.id));
+
+ teams = teams.map(team => {
+ return {
+ ...team,
+ deletable: unregisteredIds.has(team.id)
+ };
+ });
+ }
+
+ const response = teams.map(team => makeAdminTeamResponse(team));
+ res.json(response);
+ })
+);
+
+router.delete(
+ '/:teamId',
+ requirePermission('MANAGE_TEAMS'),
+ asHandler(async (req, res) => {
+ const teamId = req.params.teamId;
+ if (!teamId || typeof teamId !== 'string') {
+ res.status(400).json({ error: 'Team ID is required' });
+ return;
+ }
+
+ const team = await db.teams.byId(teamId).get();
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
+
+ const teamEvents = await db.events.byTeam(team.id).getAll();
+
+ if (teamEvents.length > 0) {
+ res.status(400).json({ error: 'Cannot delete team that is registered for an event' });
+ return;
+ }
+
+ const success = await db.teams.byId(teamId).delete();
+ if (!success) {
+ res.status(500).json({ error: 'Could not delete team' });
+ return;
+ }
+
+ res.status(200).end();
+ })
+);
+
+router.get(
+ '/:teamId',
+ asHandler(async (req, res) => {
+ const id = req.params.teamId;
+ if (!id || typeof id !== 'string') {
+ res.status(400).json({ error: 'Team ID is required' });
+ return;
+ }
+
+ const team = await db.teams.byId(id).get();
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
+ res.json(makeAdminTeamResponse(team));
+ })
+);
router.put(
'/:teamId',
requirePermission('MANAGE_TEAMS'),
fileUpload(),
- async (req: AdminRequest, res) => {
+ asHandler(async (req, res) => {
const { name, affiliation, city } = req.body;
if (!name || !affiliation || !city) {
@@ -99,6 +110,11 @@ router.put(
city
});
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
+
if (req.files && req.files.logo) {
const logoFile = req.files.logo as fileUpload.UploadedFile;
@@ -118,7 +134,12 @@ router.put(
}
try {
- team = await db.teams.byId(team.id).updateLogo(logoFile.data);
+ const updatedTeam = await db.teams.byId(team.id).updateLogo(logoFile.data);
+ if (!updatedTeam) {
+ res.status(500).json({ error: 'Failed to upload team logo' });
+ return;
+ }
+ team = updatedTeam;
} catch (error) {
console.error('Error uploading team logo:', error);
res.status(500).json({ error: 'Failed to upload team logo' });
@@ -136,14 +157,14 @@ router.put(
console.error('Error updating team:', error);
res.status(500).json({ error: 'Failed to update team' });
}
- }
+ })
);
router.post(
'/',
requirePermission('MANAGE_TEAMS'),
fileUpload(),
- async (req: AdminRequest, res) => {
+ asHandler(async (req, res) => {
const { name, number, affiliation, city, region } = req.body;
if (!name || !number || !affiliation || !city || !region) {
@@ -168,6 +189,11 @@ router.post(
logo_url: null
});
+ if (!team) {
+ res.status(500).json({ error: 'Failed to create team' });
+ return;
+ }
+
if (req.files && req.files.logo) {
const logoFile = req.files.logo as fileUpload.UploadedFile;
@@ -187,7 +213,12 @@ router.post(
}
try {
- team = await db.teams.byId(team.id).updateLogo(logoFile.data);
+ const updatedTeam = await db.teams.byId(team.id).updateLogo(logoFile.data);
+ if (!updatedTeam) {
+ res.status(500).json({ error: 'Failed to upload team logo' });
+ return;
+ }
+ team = updatedTeam;
} catch (error) {
console.error('Error uploading team logo:', error);
res.status(500).json({ error: 'Failed to upload team logo' });
@@ -209,13 +240,13 @@ router.post(
res.status(500).json({ error: 'Failed to create team' });
}
}
- }
+ })
);
router.post(
'/import',
[requirePermission('MANAGE_TEAMS'), fileUpload()],
- async (req: AdminRequest, res) => {
+ asHandler(async (req, res) => {
if (!req.files || !req.files.file) {
res.status(400).json({ error: 'No file uploaded' });
return;
@@ -239,7 +270,7 @@ router.post(
console.error('Error importing teams:', error);
res.status(500).json({ error: 'Failed to import teams' });
}
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/admin/users/index.ts b/apps/backend/src/routers/admin/users/index.ts
index 8c91b8ebb..b6c541284 100644
--- a/apps/backend/src/routers/admin/users/index.ts
+++ b/apps/backend/src/routers/admin/users/index.ts
@@ -3,6 +3,7 @@ import db from '../../../lib/database';
import { requirePermission } from '../middleware/require-permission';
import { hashPassword } from '../../../lib/security/credentials';
import { AdminRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import { makeAdminUserResponse } from './util';
import registrationRouter from './register';
import permissionsRouter from './permissions';
@@ -12,77 +13,90 @@ const router = express.Router({ mergeParams: true });
router.use('/register', registrationRouter);
router.use('/permissions', permissionsRouter);
-router.get('/', async (req: AdminRequest, res) => {
- const users = await db.admins.getAll();
- res.json(users.map(user => makeAdminUserResponse(user)));
-});
-
-router.get('/me', async (req: AdminRequest, res) => {
- const user = await db.admins.byId(req.userId).get();
-
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
-
- res.json(makeAdminUserResponse(user));
-});
-
-router.get('/:userId', async (req: AdminRequest, res) => {
- const userId = req.params.userId;
- if (!userId || typeof userId !== 'string') {
- res.status(400).json({ error: 'User ID is required' });
- return;
- }
-
- const user = await db.admins.byId(userId).get();
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
-
- res.json(makeAdminUserResponse(user));
-});
-
-router.patch('/:userId', requirePermission('MANAGE_USERS'), async (req: AdminRequest, res) => {
- const userId = req.params.userId;
- const { firstName, lastName } = req.body;
-
- if (!userId || typeof userId !== 'string') {
- res.status(400).json({ error: 'User ID is required' });
- return;
- }
-
- if (!firstName && !lastName) {
- res.status(400).json({ error: 'At least one field to update is required' });
- return;
- }
-
- const user = await db.admins.byId(userId).get();
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
-
- try {
- await db.admins.byId(userId).updateProfile({ firstName, lastName });
-
- const updatedUser = await db.admins.byId(userId).get();
- if (!updatedUser) {
- throw new Error('Failed to retrieve updated user');
+router.get(
+ '/',
+ asHandler(async (req, res) => {
+ const users = await db.admins.getAll();
+ res.json(users.map(user => makeAdminUserResponse(user)));
+ })
+);
+
+router.get(
+ '/me',
+ asHandler(async (req, res) => {
+ const user = await db.admins.byId(req.userId).get();
+
+ if (!user) {
+ res.status(404).json({ error: 'User not found' });
+ return;
+ }
+
+ res.json(makeAdminUserResponse(user));
+ })
+);
+
+router.get(
+ '/:userId',
+ asHandler(async (req, res) => {
+ const userId = req.params.userId;
+ if (!userId || typeof userId !== 'string') {
+ res.status(400).json({ error: 'User ID is required' });
+ return;
+ }
+
+ const user = await db.admins.byId(userId).get();
+ if (!user) {
+ res.status(404).json({ error: 'User not found' });
+ return;
+ }
+
+ res.json(makeAdminUserResponse(user));
+ })
+);
+
+router.patch(
+ '/:userId',
+ requirePermission('MANAGE_USERS'),
+ asHandler(async (req, res) => {
+ const userId = req.params.userId;
+ const { firstName, lastName } = req.body;
+
+ if (!userId || typeof userId !== 'string') {
+ res.status(400).json({ error: 'User ID is required' });
+ return;
+ }
+
+ if (!firstName && !lastName) {
+ res.status(400).json({ error: 'At least one field to update is required' });
+ return;
+ }
+
+ const user = await db.admins.byId(userId).get();
+ if (!user) {
+ res.status(404).json({ error: 'User not found' });
+ return;
}
- res.json(makeAdminUserResponse(updatedUser));
- } catch (error) {
- console.error('Error updating user:', error);
- res.status(500).json({ error: 'Failed to update user' });
- }
-});
+ try {
+ await db.admins.byId(userId).updateProfile({ firstName, lastName });
+
+ const updatedUser = await db.admins.byId(userId).get();
+ if (!updatedUser) {
+ throw new Error('Failed to retrieve updated user');
+ }
+
+ res.json(makeAdminUserResponse(updatedUser));
+ } catch (error) {
+ console.error('Error updating user:', error);
+ res.status(500).json({ error: 'Failed to update user' });
+ }
+ })
+);
router.patch(
'/:userId/password',
requirePermission('MANAGE_USERS'),
- async (req: AdminRequest, res) => {
+ asHandler(async (req, res) => {
const userId = req.params.userId;
const { password } = req.body;
@@ -111,44 +125,48 @@ router.patch(
console.error('Error updating password:', error);
res.status(500).json({ error: 'Failed to update password' });
}
- }
+ })
);
-router.delete('/:userId', requirePermission('MANAGE_USERS'), async (req: AdminRequest, res) => {
- const userId = req.params.userId;
-
- if (!userId || typeof userId !== 'string') {
- res.status(400).json({ error: 'User ID is required' });
- return;
- }
-
- if (userId === req.userId) {
- res.status(403).json({ error: 'CANNOT_DELETE_SELF' });
- return;
- }
-
- const user = await db.admins.byId(userId).get();
- if (!user) {
- res.status(404).json({ error: 'User not found' });
- return;
- }
-
- try {
- const userEvents = await db.admins.byId(user.id).getEvents();
- for (const event of userEvents) {
- const eventAdmins = await db.admins.byEventId(event.id).getAll();
- await db.events.byId(event.id).removeAdmin(user.id);
- if (eventAdmins.length <= 1) {
- await db.events.byId(event.id).addAdmin(req.userId);
- }
+router.delete(
+ '/:userId',
+ requirePermission('MANAGE_USERS'),
+ asHandler(async (req, res) => {
+ const userId = req.params.userId;
+
+ if (!userId || typeof userId !== 'string') {
+ res.status(400).json({ error: 'User ID is required' });
+ return;
}
- await db.admins.byId(userId).delete();
- res.status(204).send();
- } catch (error) {
- console.error('Error deleting user:', error);
- res.status(500).json({ error: 'Failed to delete user' });
- }
-});
+ if (userId === req.userId) {
+ res.status(403).json({ error: 'CANNOT_DELETE_SELF' });
+ return;
+ }
+
+ const user = await db.admins.byId(userId).get();
+ if (!user) {
+ res.status(404).json({ error: 'User not found' });
+ return;
+ }
+
+ try {
+ const userEvents = await db.admins.byId(user.id).getEvents();
+ for (const event of userEvents) {
+ const eventAdmins = await db.admins.byEventId(event.id).getAll();
+ await db.events.byId(event.id).removeAdmin(user.id);
+ if (eventAdmins.length <= 1) {
+ await db.events.byId(event.id).addAdmin(req.userId);
+ }
+ }
+
+ await db.admins.byId(userId).delete();
+ res.status(204).send();
+ } catch (error) {
+ console.error('Error deleting user:', error);
+ res.status(500).json({ error: 'Failed to delete user' });
+ }
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/users/permissions.ts b/apps/backend/src/routers/admin/users/permissions.ts
index 872467eb9..ed1849dad 100644
--- a/apps/backend/src/routers/admin/users/permissions.ts
+++ b/apps/backend/src/routers/admin/users/permissions.ts
@@ -3,71 +3,82 @@ import { ALL_ADMIN_PERMISSIONS } from '@lems/types/api/admin';
import db from '../../../lib/database';
import { AdminRequest } from '../../../types/express';
import { requirePermission } from '../middleware/require-permission';
+import { asHandler } from '../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
-router.get('/me', async (req: AdminRequest, res) => {
- const permissions = await db.admins.byId(req.userId).getPermissions();
+router.get(
+ '/me',
+ asHandler(async (req, res) => {
+ const permissions = await db.admins.byId(req.userId).getPermissions();
- if (!permissions) {
- res.status(404).json({ error: 'Permissions not found' });
- return;
- }
-
- res.json(permissions);
-});
+ if (!permissions) {
+ res.status(404).json({ error: 'Permissions not found' });
+ return;
+ }
-router.get('/:userId', async (req: AdminRequest, res) => {
- const userId = req.params.userId;
- if (!userId || typeof userId !== 'string') {
- res.status(400).json({ error: 'User ID is required' });
- return;
- }
+ res.json(permissions);
+ })
+);
- const permissions = await db.admins.byId(userId).getPermissions();
- if (!permissions) {
- res.status(404).json({ error: 'Permissions not found for this user' });
- return;
- }
+router.get(
+ '/:userId',
+ asHandler(async (req, res) => {
+ const userId = req.params.userId;
+ if (!userId || typeof userId !== 'string') {
+ res.status(400).json({ error: 'User ID is required' });
+ return;
+ }
- res.json(permissions);
-});
+ const permissions = await db.admins.byId(userId).getPermissions();
+ if (!permissions) {
+ res.status(404).json({ error: 'Permissions not found for this user' });
+ return;
+ }
-router.put('/:userId', requirePermission('MANAGE_USERS'), async (req: AdminRequest, res) => {
- const userId = req.params.userId;
- if (!userId || typeof userId !== 'string') {
- res.status(400).json({ error: 'User ID is required' });
- return;
- }
+ res.json(permissions);
+ })
+);
- const { permissions } = req.body;
- if (!Array.isArray(permissions)) {
- res.status(400).json({ error: 'Permissions must be an array' });
- return;
- }
+router.put(
+ '/:userId',
+ requirePermission('MANAGE_USERS'),
+ asHandler(async (req, res) => {
+ const userId = req.params.userId;
+ if (!userId || typeof userId !== 'string') {
+ res.status(400).json({ error: 'User ID is required' });
+ return;
+ }
- const invalidPermissions = permissions.filter(p => !ALL_ADMIN_PERMISSIONS.includes(p));
- if (invalidPermissions.length > 0) {
- res.status(400).json({
- error: 'Invalid permissions',
- invalidPermissions
- });
- return;
- }
+ const { permissions } = req.body;
+ if (!Array.isArray(permissions)) {
+ res.status(400).json({ error: 'Permissions must be an array' });
+ return;
+ }
- try {
- const user = await db.admins.byId(userId).get();
- if (!user) {
- res.status(404).json({ error: 'User not found' });
+ const invalidPermissions = permissions.filter(p => !ALL_ADMIN_PERMISSIONS.includes(p));
+ if (invalidPermissions.length > 0) {
+ res.status(400).json({
+ error: 'Invalid permissions',
+ invalidPermissions
+ });
return;
}
- const updatedPermissions = await db.admins.byId(userId).updatePermissions(permissions);
- res.json(updatedPermissions);
- } catch (error) {
- console.error('Error updating permissions:', error);
- res.status(500).json({ error: 'Internal server error' });
- }
-});
+ try {
+ const user = await db.admins.byId(userId).get();
+ if (!user) {
+ res.status(404).json({ error: 'User not found' });
+ return;
+ }
+
+ const updatedPermissions = await db.admins.byId(userId).updatePermissions(permissions);
+ res.json(updatedPermissions);
+ } catch (error) {
+ console.error('Error updating permissions:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/admin/users/register.ts b/apps/backend/src/routers/admin/users/register.ts
index 3f44641be..02a4accf1 100644
--- a/apps/backend/src/routers/admin/users/register.ts
+++ b/apps/backend/src/routers/admin/users/register.ts
@@ -7,6 +7,7 @@ import {
} from '../../../lib/security/credentials';
import { AdminRequest } from '../../../types/express';
import { requirePermission } from '../middleware/require-permission';
+import { asHandler } from '../../../types/express-handlers';
import { makeAdminUserResponse } from './util';
const router = express.Router({ mergeParams: true });
@@ -23,60 +24,72 @@ class RegistrationError extends Error {
}
}
-router.post('/', requirePermission('MANAGE_USERS'), async (req: AdminRequest, res) => {
- try {
- const { username, password, firstName, lastName } = req.body;
+router.post(
+ '/',
+ requirePermission('MANAGE_USERS'),
+ asHandler(async (req, res) => {
+ try {
+ const { username, password, firstName, lastName } = req.body;
- if (!username || !password || !firstName || !lastName) {
- throw new RegistrationError(400, 'Missing required fields', 'missing-required-fields');
- }
+ if (!username || !password || !firstName || !lastName) {
+ throw new RegistrationError(400, 'Missing required fields', 'missing-required-fields');
+ }
- const usernameValidation = validateUsername(username);
- if (!usernameValidation.isValid) {
- throw new RegistrationError(400, 'Invalid username', usernameValidation.error);
- }
+ const usernameValidation = validateUsername(username);
+ if (!usernameValidation.isValid) {
+ throw new RegistrationError(
+ 400,
+ 'Invalid username',
+ usernameValidation.error ?? 'invalid-username'
+ );
+ }
- const passwordValidation = validatePassword(password);
- if (!passwordValidation.isValid) {
- throw new RegistrationError(400, 'Invalid password', passwordValidation.error);
- }
+ const passwordValidation = validatePassword(password);
+ if (!passwordValidation.isValid) {
+ throw new RegistrationError(
+ 400,
+ 'Invalid password',
+ passwordValidation.error ?? 'invalid-password'
+ );
+ }
- if (firstName.length > 64 || lastName.length > 64) {
- throw new RegistrationError(400, 'Invalid name length', 'name-too-long');
- }
+ if (firstName.length > 64 || lastName.length > 64) {
+ throw new RegistrationError(400, 'Invalid name length', 'name-too-long');
+ }
- const existingUser = await db.admins.byUsername(username).get();
- if (existingUser) {
- throw new RegistrationError(409, 'Username already exists', 'user-already-exists');
- }
+ const existingUser = await db.admins.byUsername(username).get();
+ if (existingUser) {
+ throw new RegistrationError(409, 'Username already exists', 'user-already-exists');
+ }
- const { hash } = await hashPassword(password);
+ const { hash } = await hashPassword(password);
- const newAdminUser = await db.admins.create({
- username: username.toLowerCase(), // Store usernames in lowercase for consistency
- password_hash: hash,
- first_name: firstName.trim(),
- last_name: lastName.trim(),
- last_password_set_date: new Date()
- });
+ const newAdminUser = await db.admins.create({
+ username: username.toLowerCase(), // Store usernames in lowercase for consistency
+ password_hash: hash,
+ first_name: firstName.trim(),
+ last_name: lastName.trim(),
+ last_password_set_date: new Date()
+ });
+
+ res.status(201).json(makeAdminUserResponse(newAdminUser));
+ } catch (error) {
+ console.error('User registration error:', error);
- res.status(201).json(makeAdminUserResponse(newAdminUser));
- } catch (error) {
- console.error('User registration error:', error);
+ if (error instanceof RegistrationError) {
+ res.status(error.status).json({
+ error: error.message,
+ details: error.detail
+ });
+ return;
+ }
- if (error instanceof RegistrationError) {
- res.status(error.status).json({
- error: error.message,
- details: error.detail
+ res.status(500).json({
+ error: 'Internal server error',
+ details: 'An error occurred while creating the user'
});
- return;
}
-
- res.status(500).json({
- error: 'Internal server error',
- details: 'An error occurred while creating the user'
- });
- }
-});
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/integrations/first-israel-dashboard/index.ts b/apps/backend/src/routers/integrations/first-israel-dashboard/index.ts
index 533be2a5f..124461f99 100644
--- a/apps/backend/src/routers/integrations/first-israel-dashboard/index.ts
+++ b/apps/backend/src/routers/integrations/first-israel-dashboard/index.ts
@@ -3,6 +3,7 @@ import fileUpload, { UploadedFile } from 'express-fileupload';
import sharp from 'sharp';
import db from '../../../lib/database';
import { FirstIsraelDashboardRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import { teamLogoFileValidator, firstIsraelDashboardTeamMiddleware } from './middleware';
import eventRouter from './team/event';
@@ -14,7 +15,7 @@ router.post(
'/team/logo',
fileUpload({ limits: { fileSize: 200 * 1024, files: 1 } }),
teamLogoFileValidator,
- async (req: FirstIsraelDashboardRequest, res: Response) => {
+ asHandler(async (req, res: Response) => {
if (!req.files || !req.files.file) {
res.status(400).json({ error: 'NO_FILE_UPLOADED' });
return;
@@ -37,7 +38,7 @@ router.post(
}
res.status(200).end();
- }
+ })
);
router.use('/team/event', eventRouter);
diff --git a/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/event.ts b/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/event.ts
index b77af2fc5..170b673a9 100644
--- a/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/event.ts
+++ b/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/event.ts
@@ -7,6 +7,10 @@ import { FirstIsraelDashboardEventRequest } from '../../../../types/express';
const firstIsraelDashboardSecret = process.env.FIRST_ISRAEL_DASHBOARD_SECRET;
+if (!firstIsraelDashboardSecret) {
+ throw new Error('FIRST_ISRAEL_DASHBOARD_SECRET environment variable is required');
+}
+
export const firstIsraelDashboardEventMiddleware = async (
req: Request,
res: Response,
@@ -17,7 +21,7 @@ export const firstIsraelDashboardEventMiddleware = async (
const tokenData = jwt.verify(
token,
firstIsraelDashboardSecret
- ) as FirstIsraelDashboardTokenDataWithEvent;
+ ) as unknown as FirstIsraelDashboardTokenDataWithEvent;
const eventIntegration = await db.integrations.bySettings({ sfid: tokenData.eventSfid }).get();
if (!eventIntegration) throw new Error('Event integration not found');
diff --git a/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/team.ts b/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/team.ts
index 60814bf9f..27c8bb109 100644
--- a/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/team.ts
+++ b/apps/backend/src/routers/integrations/first-israel-dashboard/middleware/team.ts
@@ -7,6 +7,10 @@ import { FirstIsraelDashboardEventRequest } from '../../../../types/express';
const firstIsraelDashboardSecret = process.env.FIRST_ISRAEL_DASHBOARD_SECRET;
+if (!firstIsraelDashboardSecret) {
+ throw new Error('FIRST_ISRAEL_DASHBOARD_SECRET environment variable is required');
+}
+
export const firstIsraelDashboardTeamMiddleware = async (
req: Request,
res: Response,
@@ -17,7 +21,7 @@ export const firstIsraelDashboardTeamMiddleware = async (
const tokenData = jwt.verify(
token,
firstIsraelDashboardSecret
- ) as FirstIsraelDashboardTokenData;
+ ) as unknown as FirstIsraelDashboardTokenData;
const team = await db.teams.bySlug(tokenData.teamSlug).get();
if (!team) throw new Error('Team not found');
diff --git a/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/export/index.ts b/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/export/index.ts
index 17d182dcd..3406b0677 100644
--- a/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/export/index.ts
+++ b/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/export/index.ts
@@ -1,60 +1,67 @@
import express, { Response } from 'express';
import { FirstIsraelDashboardEventRequest } from '../../../../../../types/express';
import { getLemsWebpageAsPdf } from '../../../../export';
+import { asHandler } from '../../../../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
-router.get('/rubrics', async (req: FirstIsraelDashboardEventRequest, res: Response) => {
- try {
- const pdf = await getLemsWebpageAsPdf(
- `/he/lems/export/${req.teamSlug}/${req.eventSlug}/rubrics`,
- {
- teamSlug: req.teamSlug,
- divsionId: req.divisionId
- }
- );
+router.get(
+ '/rubrics',
+ asHandler(async (req, res: Response) => {
+ try {
+ const pdf = await getLemsWebpageAsPdf(
+ `/he/lems/export/${req.teamSlug}/${req.eventSlug}/rubrics`,
+ {
+ teamSlug: req.teamSlug,
+ divsionId: req.divisionId
+ }
+ );
- res.contentType('application/pdf');
- res.send(pdf);
- } catch (error) {
- console.error(
- `[Export] Failed to generate rubrics PDF for team ${req.teamSlug} event ${req.eventSlug}:`,
- error instanceof Error ? error.message : String(error)
- );
- res.status(500).json({
- error: 'Failed to generate rubrics export PDF',
- details:
- error instanceof Error
- ? error.message
- : 'Unknown error occurred. Check that all rubrics are approved and data is complete.'
- });
- }
-});
+ res.contentType('application/pdf');
+ res.send(pdf);
+ } catch (error) {
+ console.error(
+ `[Export] Failed to generate rubrics PDF for team ${req.teamSlug} event ${req.eventSlug}:`,
+ error instanceof Error ? error.message : String(error)
+ );
+ res.status(500).json({
+ error: 'Failed to generate rubrics export PDF',
+ details:
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred. Check that all rubrics are approved and data is complete.'
+ });
+ }
+ })
+);
-router.get('/scoresheets', async (req: FirstIsraelDashboardEventRequest, res: Response) => {
- try {
- const pdf = await getLemsWebpageAsPdf(
- `/he/lems/export/${req.teamSlug}/${req.eventSlug}/scoresheets`,
- {
- teamSlug: req.teamSlug,
- divsionId: req.divisionId
- }
- );
+router.get(
+ '/scoresheets',
+ asHandler(async (req, res: Response) => {
+ try {
+ const pdf = await getLemsWebpageAsPdf(
+ `/he/lems/export/${req.teamSlug}/${req.eventSlug}/scoresheets`,
+ {
+ teamSlug: req.teamSlug,
+ divsionId: req.divisionId
+ }
+ );
- res.contentType('application/pdf');
- res.send(pdf);
- } catch (error) {
- console.error(
- `[Export] Failed to generate scoresheets PDF for team ${req.teamSlug} event ${req.eventSlug}:`,
- error instanceof Error ? error.message : String(error)
- );
- res.status(500).json({
- error: 'Failed to generate scoresheets export PDF',
- details:
- error instanceof Error
- ? error.message
- : 'Unknown error occurred. Check that all scoresheets are submitted and data is complete.'
- });
- }
-});
+ res.contentType('application/pdf');
+ res.send(pdf);
+ } catch (error) {
+ console.error(
+ `[Export] Failed to generate scoresheets PDF for team ${req.teamSlug} event ${req.eventSlug}:`,
+ error instanceof Error ? error.message : String(error)
+ );
+ res.status(500).json({
+ error: 'Failed to generate scoresheets export PDF',
+ details:
+ error instanceof Error
+ ? error.message
+ : 'Unknown error occurred. Check that all scoresheets are submitted and data is complete.'
+ });
+ }
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/index.ts b/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/index.ts
index c477343a6..f56ae92bd 100644
--- a/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/index.ts
+++ b/apps/backend/src/routers/integrations/first-israel-dashboard/team/event/index.ts
@@ -5,6 +5,7 @@ import { firstIsraelDashboardEventMiddleware, teamDocumentFileValidator } from '
import db from '../../../../../lib/database';
import { uploadFile } from '../../../../../lib/blob-storage/upload';
import { FirstIsraelDashboardEventRequest } from '../../../../../types/express';
+import { asHandler } from '../../../../../types/express-handlers';
import exportRouter from './export';
const router = express.Router({ mergeParams: true });
@@ -15,7 +16,7 @@ router.post(
'/team-info',
fileUpload({ limits: { fileSize: 15 * 1024 * 1024, files: 1 } }),
teamDocumentFileValidator,
- async (req: FirstIsraelDashboardEventRequest, res: Response) => {
+ asHandler(async (req, res: Response) => {
try {
const team = await db.teams.bySlug(req.teamSlug).get();
if (!team) {
@@ -35,7 +36,7 @@ router.post(
return;
}
- const documentFile = req.files.file as UploadedFile;
+ const documentFile = req.files?.file as UploadedFile | undefined;
if (!documentFile) {
res.status(400).json({ error: 'NO_FILE_UPLOADED' });
return;
@@ -64,7 +65,7 @@ router.post(
res.status(500).json({ error: 'SERVER_ERROR' });
return;
}
- }
+ })
);
router.use('/export', exportRouter);
diff --git a/apps/backend/src/routers/integrations/sendgrid/index.ts b/apps/backend/src/routers/integrations/sendgrid/index.ts
index 9c679bfc5..e843d173e 100644
--- a/apps/backend/src/routers/integrations/sendgrid/index.ts
+++ b/apps/backend/src/routers/integrations/sendgrid/index.ts
@@ -5,6 +5,7 @@ import { requirePermission } from '../../../routers/admin/middleware/require-per
import { attachEvent } from '../../../routers/admin/middleware/attach-event';
import { authMiddleware as adminAuth } from '../../../routers/admin/middleware/auth';
import db from '../../../lib/database';
+import { asHandler } from '../../../types/express-handlers';
import { sendEmailWithSendGrid } from './sendgrid-lib';
import { generatePlaceholderPDF } from './placeholder-generator';
import { CSVRecord } from './types';
@@ -33,165 +34,174 @@ router.use(
requirePermission('MANAGE_EVENT_DETAILS')
);
-router.post('/:eventId/upload-contacts', async (req: AdminEventRequest, res) => {
- try {
- const { csvContent } = req.body;
- if (!csvContent) {
- res.status(400).json({ error: 'No CSV content provided' });
- return;
- }
-
- const csvText =
- typeof csvContent === 'string' ? csvContent : Buffer.from(csvContent).toString('utf-8');
+router.post(
+ '/:eventId/upload-contacts',
+ asHandler(async (req, res) => {
+ try {
+ const { csvContent } = req.body;
+ if (!csvContent) {
+ res.status(400).json({ error: 'No CSV content provided' });
+ return;
+ }
- const records = parse(csvText, {
- columns: ['team_number', 'region', 'recipient_email'],
- skip_empty_lines: true,
- from_line: 2 // Skip header row
- }) as CSVRecord[];
+ const csvText =
+ typeof csvContent === 'string' ? csvContent : Buffer.from(csvContent).toString('utf-8');
- if (!Array.isArray(records) || records.length === 0) {
- res.status(400).json({ error: 'CSV file is empty or invalid' });
- return;
- }
+ const records = parse(csvText, {
+ columns: ['team_number', 'region', 'recipient_email'],
+ skip_empty_lines: true,
+ from_line: 2 // Skip header row
+ }) as CSVRecord[];
- const contacts: Contact[] = [];
- const errors: ContactError[] = [];
-
- records.forEach((record, index) => {
- const result = validateContact(record, index + 2); // +2: header (1) + 0-indexed (1)
- if ('rowIndex' in result) {
- errors.push(result);
- } else {
- contacts.push(result);
+ if (!Array.isArray(records) || records.length === 0) {
+ res.status(400).json({ error: 'CSV file is empty or invalid' });
+ return;
}
- });
- if (contacts.length === 0) {
- res.status(400).json({
- error: 'No valid email addresses found in CSV',
- errorDetails: errors
- });
- return;
- }
+ const contacts: Contact[] = [];
+ const errors: ContactError[] = [];
- const eventId = (req as AdminEventRequest).eventId;
- const integration = await db.integrations.byType(eventId, 'sendgrid').get();
- if (!integration) {
- res.status(404).json({ error: 'SendGrid integration not found' });
- return;
- }
+ records.forEach((record, index) => {
+ const result = validateContact(record, index + 2); // +2: header (1) + 0-indexed (1)
+ if ('rowIndex' in result) {
+ errors.push(result);
+ } else {
+ contacts.push(result);
+ }
+ });
- const existingContacts = decodeContacts(integration.settings.emailContactsData as string);
+ if (contacts.length === 0) {
+ res.status(400).json({
+ error: 'No valid email addresses found in CSV',
+ errorDetails: errors
+ });
+ return;
+ }
- const { merged, added, updated } = mergeContacts(existingContacts, contacts);
+ const eventId = (req as AdminEventRequest).eventId;
+ const integration = await db.integrations.byType(eventId, 'sendgrid').get();
+ if (!integration) {
+ res.status(404).json({ error: 'SendGrid integration not found' });
+ return;
+ }
- const emailContactsData = encodeContacts(merged);
- const updatedSettings = {
- ...integration.settings,
- emailContactsData
- };
+ const existingContacts = decodeContacts(integration.settings.emailContactsData as string);
- await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings });
+ const { merged, added, updated } = mergeContacts(existingContacts, contacts);
- const summary: UploadSummary = {
- added,
- updated,
- errors,
- total: merged.length
- };
+ const emailContactsData = encodeContacts(merged);
+ const updatedSettings = {
+ ...integration.settings,
+ emailContactsData
+ };
- res.json(summary);
- } catch (error) {
- console.error('Error uploading contacts:', error);
- res.status(500).json({ error: 'Failed to process CSV file' });
- }
-});
+ await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings });
-router.delete('/:eventId/contacts/:teamNumber', async (req: AdminEventRequest, res) => {
- try {
- const { teamNumber } = req.params;
- const teamNum = parseInt(String(teamNumber), 10);
+ const summary: UploadSummary = {
+ added,
+ updated,
+ errors,
+ total: merged.length
+ };
- if (isNaN(teamNum)) {
- res.status(400).json({ error: 'Invalid team number' });
- return;
+ res.json(summary);
+ } catch (error) {
+ console.error('Error uploading contacts:', error);
+ res.status(500).json({ error: 'Failed to process CSV file' });
}
+ })
+);
- const eventId = (req as AdminEventRequest).eventId;
- const integration = await db.integrations.byType(eventId, 'sendgrid').get();
- if (!integration) {
- res.status(404).json({ error: 'SendGrid integration not found' });
- return;
- }
+router.delete(
+ '/:eventId/contacts/:teamNumber',
+ asHandler(async (req, res) => {
+ try {
+ const { teamNumber } = req.params;
+ const teamNum = parseInt(String(teamNumber), 10);
- const contacts = decodeContacts(integration.settings.emailContactsData as string);
+ if (isNaN(teamNum)) {
+ res.status(400).json({ error: 'Invalid team number' });
+ return;
+ }
- const filtered = contacts.filter(c => c.team_number !== teamNum);
+ const eventId = (req as AdminEventRequest).eventId;
+ const integration = await db.integrations.byType(eventId, 'sendgrid').get();
+ if (!integration) {
+ res.status(404).json({ error: 'SendGrid integration not found' });
+ return;
+ }
- const emailContactsData = encodeContacts(filtered);
- const updatedSettings = {
- ...integration.settings,
- emailContactsData
- };
+ const contacts = decodeContacts(integration.settings.emailContactsData as string);
- await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings });
+ const filtered = contacts.filter(c => c.team_number !== teamNum);
- res.json({ success: true, total: filtered.length });
- } catch (error) {
- console.error('Error deleting contact:', error);
- res.status(500).json({ error: 'Failed to delete contact' });
- }
-});
+ const emailContactsData = encodeContacts(filtered);
+ const updatedSettings = {
+ ...integration.settings,
+ emailContactsData
+ };
-router.post('/:eventId/send-test', async (req: AdminEventRequest, res) => {
- try {
- const { templateId, fromAddress, testEmailAddress } = req.body;
+ await db.integrations.byId(integration.pk.toString()).update({ settings: updatedSettings });
- if (!templateId || !fromAddress || !testEmailAddress) {
- res.status(400).json({ error: 'Missing required settings' });
- return;
+ res.json({ success: true, total: filtered.length });
+ } catch (error) {
+ console.error('Error deleting contact:', error);
+ res.status(500).json({ error: 'Failed to delete contact' });
}
+ })
+);
- const apiKey = process.env.SENDGRID_API_KEY;
- if (!apiKey) {
- console.log('SendGrid API key not configured');
- res.status(500).json({ error: 'SendGrid API key not configured' });
- return;
- }
+router.post(
+ '/:eventId/send-test',
+ asHandler(async (req, res) => {
+ try {
+ const { templateId, fromAddress, testEmailAddress } = req.body;
+
+ if (!templateId || !fromAddress || !testEmailAddress) {
+ res.status(400).json({ error: 'Missing required settings' });
+ return;
+ }
+
+ const apiKey = process.env.SENDGRID_API_KEY;
+ if (!apiKey) {
+ console.log('SendGrid API key not configured');
+ res.status(500).json({ error: 'SendGrid API key not configured' });
+ return;
+ }
+
+ const pdfBase64 = await generatePlaceholderPDF();
- const pdfBase64 = await generatePlaceholderPDF();
-
- await sendEmailWithSendGrid({
- apiKey,
- from: fromAddress,
- to: testEmailAddress,
- templateId,
- dynamicTemplateData: {
- eventName: 'Test Event',
- teamNumber: 0
- },
- attachments: [
- {
- filename: 'scoresheet.pdf',
- content: pdfBase64,
- type: 'application/pdf'
+ await sendEmailWithSendGrid({
+ apiKey,
+ from: fromAddress,
+ to: testEmailAddress,
+ templateId,
+ dynamicTemplateData: {
+ eventName: 'Test Event',
+ teamNumber: 0
},
- {
- filename: 'rubric.pdf',
- content: pdfBase64,
- type: 'application/pdf'
- }
- ]
- });
-
- res.json({ success: true });
- } catch (error) {
- console.error('Error sending test email:', error);
- res
- .status(500)
- .json({ error: error instanceof Error ? error.message : 'Failed to send test email' });
- }
-});
+ attachments: [
+ {
+ filename: 'scoresheet.pdf',
+ content: pdfBase64,
+ type: 'application/pdf'
+ },
+ {
+ filename: 'rubric.pdf',
+ content: pdfBase64,
+ type: 'application/pdf'
+ }
+ ]
+ });
+
+ res.json({ success: true });
+ } catch (error) {
+ console.error('Error sending test email:', error);
+ res
+ .status(500)
+ .json({ error: error instanceof Error ? error.message : 'Failed to send test email' });
+ }
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/lems/auth/index.ts b/apps/backend/src/routers/lems/auth/index.ts
index 5721f1835..4229aac59 100644
--- a/apps/backend/src/routers/lems/auth/index.ts
+++ b/apps/backend/src/routers/lems/auth/index.ts
@@ -32,8 +32,12 @@ router.post('/login', loginRateLimiter, async (req: Request, res: Response, next
const { captchaToken, ...loginDetails }: LoginRequest = req.body;
if (process.env.RECAPTCHA === 'true') {
+ if (!captchaToken) {
+ res.status(400).json({ error: 'CAPTCHA_REQUIRED' });
+ return;
+ }
const captcha: RecaptchaResponse = await getRecaptchaResponse(captchaToken);
- if (!captcha.success || captcha['error-codes']?.length > 0) {
+ if (!captcha.success || (captcha['error-codes']?.length ?? 0) > 0) {
logger.warn(
{
component: 'auth',
diff --git a/apps/backend/src/routers/lems/export/index.ts b/apps/backend/src/routers/lems/export/index.ts
index 4752fa51c..13a3e2aba 100644
--- a/apps/backend/src/routers/lems/export/index.ts
+++ b/apps/backend/src/routers/lems/export/index.ts
@@ -5,6 +5,7 @@ import { extractToken } from '../../../lib/security/auth';
import { logger } from '../../../lib/logger';
import db from '../../../lib/database';
import { ExportRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
const router = express.Router({ mergeParams: true });
@@ -79,142 +80,148 @@ router.use('/:teamSlug/:eventSlug', async (req: Request, res: Response, next: Ne
res.status(401).json({ error: 'UNAUTHORIZED' });
});
-router.get('/:teamSlug/:eventSlug/scoresheets', async (req: ExportRequest, res: Response) => {
- const { team, event, divisionId } = req;
+router.get(
+ '/:teamSlug/:eventSlug/scoresheets',
+ asHandler(async (req, res: Response) => {
+ const { team, event, divisionId } = req;
- const division = await db.divisions.byId(divisionId).get();
- if (!division) {
- res.status(404).json({ error: 'Division not found' });
- return;
- }
+ const division = await db.divisions.byId(divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
- const season = await db.seasons.byId(event.season_id).get();
- if (!season) {
- res.status(404).json({ error: 'Season not found' });
- return;
- }
+ const season = await db.seasons.byId(event.season_id).get();
+ if (!season) {
+ res.status(404).json({ error: 'Season not found' });
+ return;
+ }
- const scoresheets = (
- await db.scoresheets.byDivision(division.id).byTeamId(team.id).getAll()
- ).filter(s => s.stage === 'RANKING' && s.status === 'submitted');
-
- res.json({
- teamNumber: team.number,
- teamName: team.name,
- teamLogoUrl: team.logo_url,
- eventName: event.name,
- divisionName: division.name,
- seasonName: season.name,
- scoresheets: scoresheets.map(s => {
- // Gracefully handle missing data object
- if (!s.data) {
- logger.warn(
- { scoresheetId: s._id, teamId: team.id, round: s.round },
- 'Scoresheet missing data object during export'
- );
- return {
- round: s.round,
- missions: [],
- score: 0
- };
- }
-
- // Transform missions object to preserve mission IDs and convert clause values
- const missions = s.data.missions || {};
- const transformedMissions: Array<{
- id: string;
- clauses: Array<{ value: ScoresheetClauseValue }>;
- }> = [];
-
- for (const [missionId, clauses] of Object.entries(missions)) {
- const clausesArray: Array<{ value: ScoresheetClauseValue }> = [];
- for (const [indexStr, value] of Object.entries(clauses)) {
- const index = Number(indexStr);
- // Fill any gaps with null placeholders to preserve clause positions
- while (clausesArray.length < index) {
- clausesArray.push({ value: null });
+ const scoresheets = (
+ await db.scoresheets.byDivision(division.id).byTeamId(team.id).getAll()
+ ).filter(s => s.stage === 'RANKING' && s.status === 'submitted');
+
+ res.json({
+ teamNumber: team.number,
+ teamName: team.name,
+ teamLogoUrl: team.logo_url,
+ eventName: event.name,
+ divisionName: division.name,
+ seasonName: season.name,
+ scoresheets: scoresheets.map(s => {
+ // Gracefully handle missing data object
+ if (!s.data) {
+ logger.warn(
+ { scoresheetId: s._id, teamId: team.id, round: s.round },
+ 'Scoresheet missing data object during export'
+ );
+ return {
+ round: s.round,
+ missions: [],
+ score: 0
+ };
+ }
+
+ // Transform missions object to preserve mission IDs and convert clause values
+ const missions = s.data.missions || {};
+ const transformedMissions: Array<{
+ id: string;
+ clauses: Array<{ value: ScoresheetClauseValue }>;
+ }> = [];
+
+ for (const [missionId, clauses] of Object.entries(missions)) {
+ const clausesArray: Array<{ value: ScoresheetClauseValue }> = [];
+ for (const [indexStr, value] of Object.entries(clauses)) {
+ const index = Number(indexStr);
+ // Fill any gaps with null placeholders to preserve clause positions
+ while (clausesArray.length < index) {
+ clausesArray.push({ value: null });
+ }
+ clausesArray[index] = { value };
}
- clausesArray[index] = { value };
+ transformedMissions.push({ id: missionId, clauses: clausesArray });
}
- transformedMissions.push({ id: missionId, clauses: clausesArray });
- }
-
- return {
- round: s.round,
- missions: transformedMissions,
- score: s.data.score ?? 0
- };
- })
- });
-});
-router.get('/:teamSlug/:eventSlug/rubrics', async (req: ExportRequest, res: Response) => {
- const { team, event, divisionId } = req;
+ return {
+ round: s.round,
+ missions: transformedMissions,
+ score: s.data.score ?? 0
+ };
+ })
+ });
+ })
+);
+
+router.get(
+ '/:teamSlug/:eventSlug/rubrics',
+ asHandler(async (req, res: Response) => {
+ const { team, event, divisionId } = req;
+
+ const division = await db.divisions.byId(divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
- const division = await db.divisions.byId(divisionId).get();
- if (!division) {
- res.status(404).json({ error: 'Division not found' });
- return;
- }
+ const season = await db.seasons.byId(event.season_id).get();
+ if (!season) {
+ res.status(404).json({ error: 'Season not found' });
+ return;
+ }
- const season = await db.seasons.byId(event.season_id).get();
- if (!season) {
- res.status(404).json({ error: 'Season not found' });
- return;
- }
+ const allRubrics = await db.rubrics.byDivision(division.id).byTeamId(team.id).getAll();
+ const rubrics = allRubrics.filter(r => r.status === 'approved');
- const allRubrics = await db.rubrics.byDivision(division.id).byTeamId(team.id).getAll();
- const rubrics = allRubrics.filter(r => r.status === 'approved');
-
- const optionalAwards = (await db.awards.byDivisionId(division.id).getAll()).filter(
- a => a.allow_nominations
- );
- const coreValuesRubric = rubrics.find(r => r.category === 'core-values');
-
- const awards = optionalAwards.reduce(
- (acc, award) => {
- acc[award.name] = coreValuesRubric?.data?.awards?.[award.name] ?? false;
- return acc;
- },
- {} as Record
- );
-
- // Log if core values rubric is missing or incomplete
- if (!coreValuesRubric) {
- logger.warn(
- { teamId: team.id, divisionId: division.id },
- 'Core values rubric not found during export'
+ const optionalAwards = (await db.awards.byDivisionId(division.id).getAll()).filter(
+ a => a.allow_nominations
);
- } else if (!coreValuesRubric.data) {
- logger.warn(
- { rubricId: coreValuesRubric._id, rubricCategory: coreValuesRubric.category },
- 'Core values rubric missing data object during export'
+ const coreValuesRubric = rubrics.find(r => r.category === 'core-values');
+
+ const awards = optionalAwards.reduce(
+ (acc, award) => {
+ acc[award.name] = coreValuesRubric?.data?.awards?.[award.name] ?? false;
+ return acc;
+ },
+ {} as Record
);
- }
- res.json({
- teamNumber: team.number,
- teamName: team.name,
- teamLogoUrl: team.logo_url,
- eventName: event.name,
- divisionName: division.name,
- seasonName: season.name,
- rubrics: rubrics.map(r => {
- if (!r.data) {
- logger.warn(
- { rubricId: r._id, category: r.category },
- 'Rubric missing data object during export'
- );
- }
-
- return {
- id: r._id,
- category: r.category,
- data: r.data
- };
- }),
- awards: awards
- });
-});
+ // Log if core values rubric is missing or incomplete
+ if (!coreValuesRubric) {
+ logger.warn(
+ { teamId: team.id, divisionId: division.id },
+ 'Core values rubric not found during export'
+ );
+ } else if (!coreValuesRubric.data) {
+ logger.warn(
+ { rubricId: coreValuesRubric._id, rubricCategory: coreValuesRubric.category },
+ 'Core values rubric missing data object during export'
+ );
+ }
+
+ res.json({
+ teamNumber: team.number,
+ teamName: team.name,
+ teamLogoUrl: team.logo_url,
+ eventName: event.name,
+ divisionName: division.name,
+ seasonName: season.name,
+ rubrics: rubrics.map(r => {
+ if (!r.data) {
+ logger.warn(
+ { rubricId: r._id, category: r.category },
+ 'Rubric missing data object during export'
+ );
+ }
+
+ return {
+ id: r._id,
+ category: r.category,
+ data: r.data
+ };
+ }),
+ awards: awards
+ });
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/portal/divisions/index.ts b/apps/backend/src/routers/portal/divisions/index.ts
index 7e3803a57..fc5483821 100644
--- a/apps/backend/src/routers/portal/divisions/index.ts
+++ b/apps/backend/src/routers/portal/divisions/index.ts
@@ -4,6 +4,7 @@ import { PortalDivisionRequest } from '../../../types/express';
import { attachDivision } from '../middleware/attach-division';
import { makePortalTeamResponse } from '../teams/util';
import { calculateRobotGameRankings } from '../utils/ranking-calculator';
+import { asHandler } from '../../../types/express-handlers';
import {
makePortalDivisionResponse,
makePortalJudgingSessionResponse,
@@ -16,89 +17,118 @@ const router = express.Router({ mergeParams: true });
router.use('/:divisionId', attachDivision());
-router.get('/:divisionId', async (req: PortalDivisionRequest, res: Response) => {
- const division = await db.divisions.byId(req.divisionId).get();
- res.status(200).json(makePortalDivisionResponse(division));
-});
-
-router.get('/:divisionId/teams', async (req: PortalDivisionRequest, res: Response) => {
- const divisionId = req.divisionId;
- const teams = await db.teams.byDivisionId(divisionId).getAll();
- res.status(200).json(teams.map(makePortalTeamResponse));
-});
-
-router.get('/:divisionId/schedule/judging', async (req: PortalDivisionRequest, res: Response) => {
- const teams = await db.teams.byDivisionId(req.divisionId).getAll();
- const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
- const judgingSchedule = await db.judgingSessions.byDivision(req.divisionId).getAll();
-
- const sessions = judgingSchedule.map(session =>
- makePortalJudgingSessionResponse(session, rooms, teams)
- );
- res.status(200).json(sessions);
-});
-
-router.get('/:divisionId/schedule/field', async (req: PortalDivisionRequest, res: Response) => {
- const teams = await db.teams.byDivisionId(req.divisionId).getAll();
- const tables = await db.tables.byDivisionId(req.divisionId).getAll();
- const fieldSchedule = await db.robotGameMatches.byDivision(req.divisionId).getAll();
-
- const matches = fieldSchedule.map(match => makePortalMatchResponse(match, tables, teams));
- res.status(200).json(matches);
-});
-
-router.get('/:divisionId/agenda', async (req: PortalDivisionRequest, res: Response) => {
- const agendaPublic = await db.divisions.byId(req.divisionId).agenda().getAll('public');
- const agendaTeams = await db.divisions.byId(req.divisionId).agenda().getAll('teams');
- const agenda = [...agendaPublic, ...agendaTeams].sort(
- (a, b) => a.start_time.getTime() - b.start_time.getTime()
- );
-
- res.status(200).json(agenda.map(makePortalAgendaResponse));
-});
+router.get(
+ '/:divisionId',
+ asHandler(async (req, res: Response) => {
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ res.status(200).json(makePortalDivisionResponse(division));
+ })
+);
+
+router.get(
+ '/:divisionId/teams',
+ asHandler(async (req, res: Response) => {
+ const divisionId = req.divisionId;
+ const teams = await db.teams.byDivisionId(divisionId).getAll();
+ res.status(200).json(teams.map(makePortalTeamResponse));
+ })
+);
+
+router.get(
+ '/:divisionId/schedule/judging',
+ asHandler(async (req, res: Response) => {
+ const teams = await db.teams.byDivisionId(req.divisionId).getAll();
+ const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
+ const judgingSchedule = await db.judgingSessions.byDivision(req.divisionId).getAll();
+
+ const sessions = judgingSchedule.map(session =>
+ makePortalJudgingSessionResponse(session, rooms, teams)
+ );
+ res.status(200).json(sessions);
+ })
+);
+
+router.get(
+ '/:divisionId/schedule/field',
+ asHandler(async (req, res: Response) => {
+ const teams = await db.teams.byDivisionId(req.divisionId).getAll();
+ const tables = await db.tables.byDivisionId(req.divisionId).getAll();
+ const fieldSchedule = await db.robotGameMatches.byDivision(req.divisionId).getAll();
+
+ const matches = fieldSchedule.map(match => makePortalMatchResponse(match, tables, teams));
+ res.status(200).json(matches);
+ })
+);
+
+router.get(
+ '/:divisionId/agenda',
+ asHandler(async (req, res: Response) => {
+ const agendaPublic = await db.divisions.byId(req.divisionId).agenda().getAll('public');
+ const agendaTeams = await db.divisions.byId(req.divisionId).agenda().getAll('teams');
+ const agenda = [...agendaPublic, ...agendaTeams].sort(
+ (a, b) => a.start_time.getTime() - b.start_time.getTime()
+ );
+
+ res.status(200).json(agenda.map(makePortalAgendaResponse));
+ })
+);
// TODO: Implement this properly
-router.get('/:divisionId/scoreboard', async (req: PortalDivisionRequest, res: Response) => {
- const teams = await db.teams.byDivisionId(req.divisionId).getAll();
-
- const rankings = await calculateRobotGameRankings(req.divisionId);
-
- const scoreboard = teams.map(team => {
- const rankingData = rankings.get(team.id);
-
- const scores =
- rankingData?.scoresWithRounds.sort((a, b) => a.round - b.round).map(s => s.score) ?? [];
-
- return {
- team: {
- id: team.id,
- name: team.name,
- number: team.number,
- affiliation: team.affiliation,
- city: team.city,
- region: team.region,
- slug: `${team.region}-${team.number}`.toUpperCase()
- },
- robotGameRank: rankingData?.rank ?? null,
- maxScore: rankingData?.maxScore ?? null,
- scores: scores.length > 0 ? scores : null
- };
- });
-
- res.status(200).json(scoreboard);
-});
-
-router.get('/:divisionId/awards', async (req: PortalDivisionRequest, res: Response) => {
- const division = await db.divisions.byId(req.divisionId).get();
- const eventSettings = await db.events.byId(division.event_id).getSettings();
-
- if (!eventSettings?.published) {
- res.status(200).json(null);
- return;
- }
-
- const awards = await db.awards.byDivisionId(req.divisionId).getAll();
- res.status(200).json(awards.map(makePortalAwardsResponse));
-});
+router.get(
+ '/:divisionId/scoreboard',
+ asHandler(async (req, res: Response) => {
+ const teams = await db.teams.byDivisionId(req.divisionId).getAll();
+
+ const rankings = await calculateRobotGameRankings(req.divisionId);
+
+ const scoreboard = teams.map(team => {
+ const rankingData = rankings.get(team.id);
+
+ const scores =
+ rankingData?.scoresWithRounds.sort((a, b) => a.round - b.round).map(s => s.score) ?? [];
+
+ return {
+ team: {
+ id: team.id,
+ name: team.name,
+ number: team.number,
+ affiliation: team.affiliation,
+ city: team.city,
+ region: team.region,
+ slug: `${team.region}-${team.number}`.toUpperCase()
+ },
+ robotGameRank: rankingData?.rank ?? null,
+ maxScore: rankingData?.maxScore ?? null,
+ scores: scores.length > 0 ? scores : null
+ };
+ });
+
+ res.status(200).json(scoreboard);
+ })
+);
+
+router.get(
+ '/:divisionId/awards',
+ asHandler(async (req, res: Response) => {
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ const eventSettings = await db.events.byId(division.event_id).getSettings();
+
+ if (!eventSettings?.published) {
+ res.status(200).json(null);
+ return;
+ }
+
+ const awards = await db.awards.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(awards.map(makePortalAwardsResponse));
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/portal/events/index.ts b/apps/backend/src/routers/portal/events/index.ts
index 1ddfad5f3..f4978ae4e 100644
--- a/apps/backend/src/routers/portal/events/index.ts
+++ b/apps/backend/src/routers/portal/events/index.ts
@@ -3,6 +3,7 @@ import { EventDetails, EventSummary } from '@lems/database';
import db from '../../../lib/database';
import { attachEvent } from '../middleware/attach-event';
import { PortalEventRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import eventTeamRouter from './teams';
import { makePortalEventDetailsResponse, makePortalEventSummaryResponse } from './util';
@@ -22,6 +23,10 @@ router.get('/', async (req: Request, res: Response) => {
const registeredTeams = await db.events.bySlug(eventSlug).getRegisteredTeams();
const divisions = await db.divisions.byEventId(event.id).getAll();
const settings = await db.events.byId(event.id).getSettings();
+ if (!settings) {
+ res.status(404).json({ error: 'Event settings not found' });
+ return;
+ }
const eventSummary: EventSummary = {
...event,
@@ -76,22 +81,37 @@ router.get('/', async (req: Request, res: Response) => {
router.use('/:slug', attachEvent());
-router.get('/:slug', async (req: PortalEventRequest, res: Response) => {
- const event = await db.events.byId(req.eventId).get();
- const divisions = await db.divisions.byEventId(event.id).getAllSummaries();
- const season = await db.seasons.byId(event.season_id).get();
- const settings = await db.events.byId(event.id).getSettings();
-
- const eventSummary: EventDetails = {
- ...event,
- divisions,
- season_name: season.name,
- season_slug: season.slug,
- official: settings.official
- };
-
- res.json(makePortalEventDetailsResponse(eventSummary));
-});
+router.get(
+ '/:slug',
+ asHandler(async (req, res: Response) => {
+ const event = await db.events.byId(req.eventId).get();
+ if (!event) {
+ res.status(404).json({ error: 'Event not found' });
+ return;
+ }
+ const divisions = await db.divisions.byEventId(event.id).getAllSummaries();
+ const season = await db.seasons.byId(event.season_id).get();
+ if (!season) {
+ res.status(404).json({ error: 'Season not found' });
+ return;
+ }
+ const settings = await db.events.byId(event.id).getSettings();
+ if (!settings) {
+ res.status(404).json({ error: 'Event settings not found' });
+ return;
+ }
+
+ const eventSummary: EventDetails = {
+ ...event,
+ divisions,
+ season_name: season.name,
+ season_slug: season.slug,
+ official: settings.official
+ };
+
+ res.json(makePortalEventDetailsResponse(eventSummary));
+ })
+);
router.use('/:slug/teams', eventTeamRouter);
diff --git a/apps/backend/src/routers/portal/events/teams/index.ts b/apps/backend/src/routers/portal/events/teams/index.ts
index 8372cd1b6..03751c20d 100644
--- a/apps/backend/src/routers/portal/events/teams/index.ts
+++ b/apps/backend/src/routers/portal/events/teams/index.ts
@@ -5,6 +5,7 @@ import { attachTeamAtEvent } from '../../middleware/attach-team-at-event';
import { makePortalAwardsResponse, makePortalDivisionResponse } from '../../divisions/util';
import { makePortalTeamResponse } from '../../teams/util';
import { getTeamRankingData } from '../../utils/ranking-calculator';
+import { asHandler } from '../../../../types/express-handlers';
import {
makePortalTeamJudgingSessionResponse,
makePortalTeamRobotGameMatchResponse,
@@ -15,72 +16,102 @@ const router = express.Router({ mergeParams: true });
router.use('/:teamSlug', attachTeamAtEvent());
-router.get('/:teamSlug', async (req: PortalTeamAtEventRequest, res: Response) => {
- const team = await db.teams.byId(req.teamId).get();
- const division = await db.divisions.byId(req.divisionId).get();
- const event = await db.events.byId(division.event_id).get();
+router.get(
+ '/:teamSlug',
+ asHandler(async (req, res: Response) => {
+ const team = await db.teams.byId(req.teamId).get();
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ const event = await db.events.byId(division.event_id).get();
+ if (!event) {
+ res.status(404).json({ error: 'Event not found' });
+ return;
+ }
- res.json({
- team: makePortalTeamResponse(team),
- event: {
- id: event.id,
- name: event.name,
- slug: event.slug
- },
- division: makePortalDivisionResponse(division)
- });
-});
+ res.json({
+ team: makePortalTeamResponse(team),
+ event: {
+ id: event.id,
+ name: event.name,
+ slug: event.slug
+ },
+ division: makePortalDivisionResponse(division)
+ });
+ })
+);
-router.get('/:teamSlug/activities', async (req: PortalTeamAtEventRequest, res: Response) => {
- const session = await db.judgingSessions.byDivision(req.divisionId).getByTeam(req.teamId);
- const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
- const matches = await db.robotGameMatches.byDivision(req.divisionId).getByTeam(req.teamId);
- const tables = await db.tables.byDivisionId(req.divisionId).getAll();
- const agendaPublic = await db.divisions.byId(req.divisionId).agenda().getAll('public');
- const agendaTeams = await db.divisions.byId(req.divisionId).agenda().getAll('teams');
- const agenda = [...agendaPublic, ...agendaTeams];
+router.get(
+ '/:teamSlug/activities',
+ asHandler(async (req, res: Response) => {
+ const session = await db.judgingSessions.byDivision(req.divisionId).getByTeam(req.teamId);
+ const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
+ const matches = await db.robotGameMatches.byDivision(req.divisionId).getByTeam(req.teamId);
+ const tables = await db.tables.byDivisionId(req.divisionId).getAll();
+ const agendaPublic = await db.divisions.byId(req.divisionId).agenda().getAll('public');
+ const agendaTeams = await db.divisions.byId(req.divisionId).agenda().getAll('teams');
+ const agenda = [...agendaPublic, ...agendaTeams];
- res.json({
- session: makePortalTeamJudgingSessionResponse(req.teamId, session, rooms),
- matches: matches.map(match => makePortalTeamRobotGameMatchResponse(req.teamId, match, tables)),
- agenda: agenda.map(a => makeAgendaResponse(a))
- });
-});
+ res.json({
+ session: session ? makePortalTeamJudgingSessionResponse(req.teamId, session, rooms) : null,
+ matches: matches.map(match =>
+ makePortalTeamRobotGameMatchResponse(req.teamId, match, tables)
+ ),
+ agenda: agenda.map(a => makeAgendaResponse(a))
+ });
+ })
+);
-router.get('/:teamSlug/awards', async (req: PortalTeamAtEventRequest, res: Response) => {
- const division = await db.divisions.byId(req.divisionId).get();
- const eventSettings = await db.events.byId(division.event_id).getSettings();
+router.get(
+ '/:teamSlug/awards',
+ asHandler(async (req, res: Response) => {
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ const eventSettings = await db.events.byId(division.event_id).getSettings();
- if (!eventSettings.published) {
- res.status(200).json([]);
- return;
- }
+ if (!eventSettings?.published) {
+ res.status(200).json([]);
+ return;
+ }
- const teamAwards = await db.awards.byDivisionId(division.id).getByTeam(req.teamId);
- res.status(200).json(teamAwards.map(makePortalAwardsResponse));
-});
+ const teamAwards = await db.awards.byDivisionId(division.id).getByTeam(req.teamId);
+ res.status(200).json(teamAwards.map(makePortalAwardsResponse));
+ })
+);
-router.get('/:teamSlug/robot-performance', async (req: PortalTeamAtEventRequest, res: Response) => {
- const rankingData = await getTeamRankingData(req.divisionId, req.teamId);
+router.get(
+ '/:teamSlug/robot-performance',
+ asHandler(async (req, res: Response) => {
+ const rankingData = await getTeamRankingData(req.divisionId, req.teamId);
- if (!rankingData) {
- res.status(200).json({
- scores: [],
- highestScore: null,
- robotGameRank: null
- });
- return;
- }
+ if (!rankingData) {
+ res.status(200).json({
+ scores: [],
+ highestScore: null,
+ robotGameRank: null
+ });
+ return;
+ }
- const sortedScores = rankingData.scoresWithRounds
- .sort((a, b) => a.round - b.round)
- .map(s => s.score);
+ const sortedScores = rankingData.scoresWithRounds
+ .sort((a, b) => a.round - b.round)
+ .map(s => s.score);
- res.status(200).json({
- scores: sortedScores,
- highestScore: rankingData.maxScore,
- robotGameRank: rankingData.rank
- });
-});
+ res.status(200).json({
+ scores: sortedScores,
+ highestScore: rankingData.maxScore,
+ robotGameRank: rankingData.rank
+ });
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/portal/middleware/attach-division.ts b/apps/backend/src/routers/portal/middleware/attach-division.ts
index 8b94c12c9..4f4761d78 100644
--- a/apps/backend/src/routers/portal/middleware/attach-division.ts
+++ b/apps/backend/src/routers/portal/middleware/attach-division.ts
@@ -24,7 +24,7 @@ export const attachDivision = () => {
const eventSettings = await database.events.byId(division.event_id).getSettings();
- if (!eventSettings.visible) {
+ if (!eventSettings?.visible) {
res.status(404).json({ error: 'DIVISION_NOT_FOUND' });
return;
}
diff --git a/apps/backend/src/routers/portal/middleware/attach-event.ts b/apps/backend/src/routers/portal/middleware/attach-event.ts
index d1e85c9d5..11c544739 100644
--- a/apps/backend/src/routers/portal/middleware/attach-event.ts
+++ b/apps/backend/src/routers/portal/middleware/attach-event.ts
@@ -16,7 +16,7 @@ export const attachEvent = () => {
return;
}
- let settings: EventSettings;
+ let settings: EventSettings | null = null;
try {
settings = await database.events.bySlug(eventSlug).getSettings();
@@ -25,6 +25,11 @@ export const attachEvent = () => {
return;
}
+ if (!settings) {
+ res.status(404).json({ error: 'EVENT_NOT_FOUND' });
+ return;
+ }
+
if (!settings.visible) {
res.status(404).json({ error: 'EVENT_NOT_FOUND' });
return;
diff --git a/apps/backend/src/routers/portal/seasons/index.ts b/apps/backend/src/routers/portal/seasons/index.ts
index a0d66bacd..5b12e73c5 100644
--- a/apps/backend/src/routers/portal/seasons/index.ts
+++ b/apps/backend/src/routers/portal/seasons/index.ts
@@ -11,7 +11,12 @@ router.get('/latest', async (req: Request, res: Response) => {
return;
}
- const latestSeason = await db.seasons.getAll()[0];
+ const allSeasons = await db.seasons.getAll();
+ const latestSeason = allSeasons[0];
+ if (!latestSeason) {
+ res.status(404).json({ message: 'No seasons found' });
+ return;
+ }
res.status(200).json(makePortalSeasonResponse(latestSeason));
});
diff --git a/apps/backend/src/routers/portal/teams/index.ts b/apps/backend/src/routers/portal/teams/index.ts
index 88b9f9622..567ae8a70 100644
--- a/apps/backend/src/routers/portal/teams/index.ts
+++ b/apps/backend/src/routers/portal/teams/index.ts
@@ -7,6 +7,7 @@ import { makePortalSeasonResponse } from '../seasons/util';
import { attachTeam } from '../middleware/attach-team';
import { PortalTeamRequest } from '../../../types/express';
import { getTeamRankingData } from '../utils/ranking-calculator';
+import { asHandler, asMiddleware } from '../../../types/express-handlers';
import { makePortalTeamResponse, makePortalTeamSummaryResponse } from './util';
const router = express.Router({ mergeParams: true });
@@ -38,44 +39,54 @@ router.use('/:teamSlug', attachTeam());
/**
* Returns a team, as well as the latest season they competed at
*/
-router.get('/:teamSlug/summary', async (req: PortalTeamRequest, res: Response) => {
- const team = await db.teams.byId(req.teamId).get();
+router.get(
+ '/:teamSlug/summary',
+ asHandler(async (req, res: Response) => {
+ const team = await db.teams.byId(req.teamId).get();
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
- const teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
- const seasons = await db.seasons.getAll(); // Sorted by default
+ const teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
+ const seasons = await db.seasons.getAll(); // Sorted by default
- for (const season of seasons) {
- if (teamEvents.some(event => event.season_id === season.id)) {
- const seasonResponse = makePortalSeasonResponse(season);
+ for (const season of seasons) {
+ if (teamEvents.some(event => event.season_id === season.id)) {
+ const seasonResponse = makePortalSeasonResponse(season);
- res.status(200).json(makePortalTeamSummaryResponse(team, seasonResponse));
- return;
+ res.status(200).json(makePortalTeamSummaryResponse(team, seasonResponse));
+ return;
+ }
}
- }
- res.status(200).json(makePortalTeamSummaryResponse(team, null));
-});
+ res.status(200).json(makePortalTeamSummaryResponse(team, null));
+ })
+);
/**
* Returns the seasons a team competed at
*/
-router.get('/:teamSlug/seasons', async (req: PortalTeamRequest, res: Response) => {
- const seasons = await db.seasons.getAll();
- const teamSeasons = new Set();
+router.get(
+ '/:teamSlug/seasons',
+ asHandler(async (req, res: Response) => {
+ const seasons = await db.seasons.getAll();
+ const teamSeasons = new Set();
- const teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
+ const teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
- for (const event of teamEvents) {
- if (!event.visible) continue;
+ for (const event of teamEvents) {
+ if (!event.visible) continue;
- if (!teamSeasons.has(event.season_id)) {
- teamSeasons.add(event.season_id);
+ if (!teamSeasons.has(event.season_id)) {
+ teamSeasons.add(event.season_id);
+ }
}
- }
- const filteredSeasons = seasons.filter(season => teamSeasons.has(season.id));
- res.status(200).json(filteredSeasons.map(makePortalSeasonResponse));
-});
+ const filteredSeasons = seasons.filter(season => teamSeasons.has(season.id));
+ res.status(200).json(filteredSeasons.map(makePortalSeasonResponse));
+ })
+);
type PortalTeamWithSeasonRequest = PortalTeamRequest & { seasonId?: string };
@@ -108,8 +119,8 @@ const seasonFilter = async (
*/
router.get(
'/:teamSlug/events',
- seasonFilter,
- async (req: PortalTeamRequest & { seasonId?: string }, res: Response) => {
+ asMiddleware(seasonFilter),
+ asHandler(async (req, res) => {
let teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
teamEvents = teamEvents.filter(event => event.visible);
@@ -118,7 +129,7 @@ router.get(
}
res.status(200).json(teamEvents.map(makePortalEventResponse));
- }
+ })
);
/**
@@ -126,8 +137,8 @@ router.get(
*/
router.get(
'/:teamSlug/events/results',
- seasonFilter,
- async (req: PortalTeamWithSeasonRequest, res: Response) => {
+ asMiddleware(seasonFilter),
+ asHandler(async (req, res) => {
let teamEvents = await db.events.byTeam(req.teamId).getAllSummaries();
teamEvents = teamEvents.filter(event => event.visible);
@@ -154,6 +165,10 @@ router.get(
// TODO: This isn't the most efficient way to check division registration,
// we should improve this sometime by batch-requesting
const teamDivision = await db.teams.byId(req.teamId).isInEvent(event.id);
+ if (!teamDivision) {
+ eventResults.push(eventResult);
+ continue;
+ }
const awards = await db.awards.byDivisionId(teamDivision).getAll();
const teamAwards = awards.filter(award => award.winner_id === req.teamId);
@@ -173,16 +188,20 @@ router.get(
const scoresByRound = new Map();
for (const sheet of submittedScoresheets) {
- scoresByRound.set(sheet.round, sheet.data.score);
+ scoresByRound.set(sheet.round, sheet.data?.score ?? 0);
}
- const teamMatchResults = rankingMatches.map(match => ({
- number: match.round,
- score: scoresByRound.get(match.round) ?? null
- }));
+ const teamMatchResults = rankingMatches
+ .map(match => ({
+ number: match.round,
+ score: scoresByRound.get(match.round)
+ }))
+ .filter((match): match is { number: number; score: number } => match.score != null);
+
+ const divisionTeams = await db.teams.byDivisionId(teamDivision).getAll();
const rankingData = await getTeamRankingData(teamDivision, req.teamId);
- const robotGameRank = rankingData?.rank ?? null;
+ const robotGameRank = rankingData?.rank ?? divisionTeams.length;
eventResult.results = {
awards: teamAwards.map(award => ({
@@ -197,7 +216,7 @@ router.get(
}
res.status(200).json(eventResults);
- }
+ })
);
export default router;
diff --git a/apps/backend/src/routers/portal/teams/util.ts b/apps/backend/src/routers/portal/teams/util.ts
index 9eaad8f8d..778f793c5 100644
--- a/apps/backend/src/routers/portal/teams/util.ts
+++ b/apps/backend/src/routers/portal/teams/util.ts
@@ -13,10 +13,7 @@ export const makePortalTeamResponse = (team: DbTeam): Team => ({
slug: `${team.region}-${team.number}`.toUpperCase()
});
-export const makePortalTeamSummaryResponse = (
- team: DbTeam,
- lastCompetedSeason: Season | null
-): TeamSummary => ({
+export const makePortalTeamSummaryResponse = (team: DbTeam, lastCompetedSeason: Season | null) => ({
...makePortalTeamResponse(team),
lastCompetedSeason
});
diff --git a/apps/backend/src/routers/portal/utils/ranking-calculator.ts b/apps/backend/src/routers/portal/utils/ranking-calculator.ts
index fb2f82b21..2ce6624b6 100644
--- a/apps/backend/src/routers/portal/utils/ranking-calculator.ts
+++ b/apps/backend/src/routers/portal/utils/ranking-calculator.ts
@@ -27,6 +27,10 @@ export async function calculateRobotGameRankings(
.collection('division_states')
.findOne({ divisionId });
+ if (!divisionState?.field?.currentStage) {
+ return new Map();
+ }
+
const scoresheets = await db.scoresheets
.byDivision(divisionId)
.byStage(divisionState.field.currentStage)
@@ -44,7 +48,7 @@ export async function calculateRobotGameRankings(
}
teamScores.get(scoresheet.teamId)!.push({
round: scoresheet.round,
- score: scoresheet.data.score
+ score: scoresheet.data!.score
});
}
diff --git a/apps/backend/src/routers/scheduler/divisions/index.ts b/apps/backend/src/routers/scheduler/divisions/index.ts
index 25778d4e8..6a294e52e 100644
--- a/apps/backend/src/routers/scheduler/divisions/index.ts
+++ b/apps/backend/src/routers/scheduler/divisions/index.ts
@@ -10,104 +10,124 @@ import { JUDGING_CATEGORIES } from '@lems/types/judging';
import db from '../../../lib/database';
import { attachDivision } from '../middleware/attach-division';
import { SchedulerRequest } from '../../../types/express';
+import { asHandler } from '../../../types/express-handlers';
import { makeSchedulerLocationResponse, makeSchedulerTeamResponse } from './utils';
const router = express.Router({ mergeParams: true });
router.use(attachDivision());
-router.get('/teams', async (req: SchedulerRequest, res) => {
- const teams = await db.teams.byDivisionId(req.divisionId).getAll();
- res.status(200).json(teams.map(team => makeSchedulerTeamResponse(team)));
-});
-
-router.get('/team/:teamSlug', async (req: SchedulerRequest, res) => {
- const { teamSlug } = req.params;
- if (!teamSlug || typeof teamSlug !== 'string') {
- res.status(400).json({ error: 'Team slug is required' });
- return;
- }
-
- const [, teamNumber] = teamSlug.split('-');
-
- if (Number.isNaN(Number(teamNumber))) {
- res.status(400).json({ error: 'Team number must be a number' });
- return;
- }
-
- const team = await db.teams.bySlug(teamSlug).get();
- if (!team) {
- res.status(404).json({ error: 'Team not found' });
- return;
- }
-
- const isInDivision = db.teams.byId(team.id).isInDivision(req.divisionId);
- if (!isInDivision) {
- res.status(400).json({ error: 'Team not found in this division' });
- return;
- }
-
- res.status(200).json(makeSchedulerTeamResponse(team));
-});
-
-router.get('/tables', async (req: SchedulerRequest, res) => {
- const tables = await db.tables.byDivisionId(req.divisionId).getAll();
- res.status(200).json(tables.map(table => makeSchedulerLocationResponse(table)));
-});
-
-router.get('/rooms', async (req: SchedulerRequest, res) => {
- const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
- res.status(200).json(rooms.map(room => makeSchedulerLocationResponse(room)));
-});
-
-router.post('/sessions', async (req: SchedulerRequest, res) => {
- const { sessions }: { sessions: InsertableJudgingSession[] } = req.body;
-
- if (!sessions || !Array.isArray(sessions)) {
- res.status(400).json({ error: 'Sessions are required' });
- return;
- }
-
- const division = await db.divisions.byId(req.divisionId).get();
- if (division.has_schedule) {
- res
- .status(400)
- .json({ error: 'Division already has a schedule. Delete the existing schedule first.' });
- return;
- }
-
- await db.judgingSessions.createMany(sessions);
-
- await Promise.all(
- JUDGING_CATEGORIES.map(category =>
- db.judgingDeliberations.create({
- division_id: division.id,
- category
- })
- )
- );
-
- await db.finalDeliberations.create(division.id);
-
- // Create rubrics for each team in the division
- const teamIds = [
- ...new Set(sessions.map(s => s.team_id).filter((id): id is string => id !== null))
- ];
- const categories: JudgingCategory[] = ['innovation-project', 'robot-design', 'core-values'];
-
- const rubrics: Rubric[] = teamIds.flatMap(teamId =>
- categories.map(category => ({
- divisionId: req.divisionId,
- teamId,
- category,
- status: 'empty' as const
- }))
- );
-
- await db.rubrics.createMany(rubrics);
-
- res.status(200).json({ ok: true });
-});
+router.get(
+ '/teams',
+ asHandler(async (req, res) => {
+ const teams = await db.teams.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(teams.map(team => makeSchedulerTeamResponse(team)));
+ })
+);
+
+router.get(
+ '/team/:teamSlug',
+ asHandler(async (req, res) => {
+ const { teamSlug } = req.params;
+ if (!teamSlug || typeof teamSlug !== 'string') {
+ res.status(400).json({ error: 'Team slug is required' });
+ return;
+ }
+
+ const [, teamNumber] = teamSlug.split('-');
+
+ if (Number.isNaN(Number(teamNumber))) {
+ res.status(400).json({ error: 'Team number must be a number' });
+ return;
+ }
+
+ const team = await db.teams.bySlug(teamSlug).get();
+ if (!team) {
+ res.status(404).json({ error: 'Team not found' });
+ return;
+ }
+
+ const isInDivision = db.teams.byId(team.id).isInDivision(req.divisionId);
+ if (!isInDivision) {
+ res.status(400).json({ error: 'Team not found in this division' });
+ return;
+ }
+
+ res.status(200).json(makeSchedulerTeamResponse(team));
+ })
+);
+
+router.get(
+ '/tables',
+ asHandler(async (req, res) => {
+ const tables = await db.tables.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(tables.map(table => makeSchedulerLocationResponse(table)));
+ })
+);
+
+router.get(
+ '/rooms',
+ asHandler(async (req, res) => {
+ const rooms = await db.rooms.byDivisionId(req.divisionId).getAll();
+ res.status(200).json(rooms.map(room => makeSchedulerLocationResponse(room)));
+ })
+);
+
+router.post(
+ '/sessions',
+ asHandler(async (req, res) => {
+ const { sessions }: { sessions: InsertableJudgingSession[] } = req.body;
+
+ if (!sessions || !Array.isArray(sessions)) {
+ res.status(400).json({ error: 'Sessions are required' });
+ return;
+ }
+
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ if (division.has_schedule) {
+ res
+ .status(400)
+ .json({ error: 'Division already has a schedule. Delete the existing schedule first.' });
+ return;
+ }
+
+ await db.judgingSessions.createMany(sessions);
+
+ await Promise.all(
+ JUDGING_CATEGORIES.map(category =>
+ db.judgingDeliberations.create({
+ division_id: division.id,
+ category
+ })
+ )
+ );
+
+ await db.finalDeliberations.create(division.id);
+
+ // Create rubrics for each team in the division
+ const teamIds = [
+ ...new Set(sessions.map(s => s.team_id).filter((id): id is string => id !== null))
+ ];
+ const categories: JudgingCategory[] = ['innovation-project', 'robot-design', 'core-values'];
+
+ const rubrics: Rubric[] = teamIds.flatMap(teamId =>
+ categories.map(category => ({
+ divisionId: req.divisionId,
+ teamId,
+ category,
+ status: 'empty' as const
+ }))
+ );
+
+ await db.rubrics.createMany(rubrics);
+
+ res.status(200).json({ ok: true });
+ })
+);
interface MatchRequest {
number: number;
@@ -117,133 +137,146 @@ interface MatchRequest {
tables: Record;
}
-router.post('/matches', async (req: SchedulerRequest, res) => {
- const { matches }: { matches: MatchRequest[] } = req.body;
-
- if (!matches || !Array.isArray(matches)) {
- res.status(400).json({ error: 'Matches are required' });
- return;
- }
-
- const division = await db.divisions.byId(req.divisionId).get();
- if (division.has_schedule) {
- res
- .status(400)
- .json({ error: 'Division already has a schedule. Delete the existing schedule first.' });
- return;
- }
-
- try {
- const matchesWithParticipants = matches.map(match => ({
- match: {
- number: match.number,
- round: match.round,
- stage: match.stage.toUpperCase() as 'PRACTICE' | 'RANKING' | 'TEST',
- scheduled_time: new Date(match.scheduled_time),
- division_id: req.divisionId
- } as InsertableRobotGameMatch,
- participants: Object.entries(match.tables).map(([tableId, tableData]) => ({
- team_id: tableData.team_id || null,
- table_id: tableId
- })) as InsertableRobotGameMatchParticipant[]
- }));
-
- const testMatch = {
- match: {
- number: 0,
- round: 0,
- stage: 'TEST' as 'PRACTICE' | 'RANKING' | 'TEST',
- scheduled_time: new Date(),
- division_id: req.divisionId
- } as InsertableRobotGameMatch,
- participants: [] as InsertableRobotGameMatchParticipant[]
- };
-
- const allMatches = [testMatch, ...matchesWithParticipants];
-
- await db.robotGameMatches.createMany(allMatches);
-
- const scoresheets = [];
-
- for (const { match, participants } of matchesWithParticipants) {
- for (const participant of participants) {
- if (participant.team_id) {
- scoresheets.push({
- divisionId: req.divisionId,
- teamId: participant.team_id,
- stage: match.stage as 'PRACTICE' | 'RANKING',
- round: match.round,
- status: 'empty' as const,
- escalated: false
- });
- }
- }
+router.post(
+ '/matches',
+ asHandler(async (req, res) => {
+ const { matches }: { matches: MatchRequest[] } = req.body;
+
+ if (!matches || !Array.isArray(matches)) {
+ res.status(400).json({ error: 'Matches are required' });
+ return;
}
- await db.scoresheets.createMany(scoresheets);
+ const division = await db.divisions.byId(req.divisionId).get();
+ if (!division) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+ if (division.has_schedule) {
+ res
+ .status(400)
+ .json({ error: 'Division already has a schedule. Delete the existing schedule first.' });
+ return;
+ }
- res.status(200).json({ ok: true });
- } catch (error) {
- console.error('Error creating matches:', error);
- res.status(500).json({ error: 'Failed to create matches' });
- }
-});
-
-router.put('/settings', async (req: SchedulerRequest, res) => {
- try {
- const { schedule_settings } = req.body;
-
- if (schedule_settings !== undefined && schedule_settings !== null) {
- const requiredFields = [
- 'match_length',
- 'practice_cycle_time',
- 'ranking_cycle_time',
- 'judging_session_length',
- 'judging_session_cycle_time'
- ];
-
- for (const field of requiredFields) {
- if (!(field in schedule_settings) || typeof schedule_settings[field] !== 'number') {
- res.status(400).json({
- error: `Invalid schedule_settings: ${field} must be a number`
- });
- return;
+ try {
+ const matchesWithParticipants = matches.map(match => ({
+ match: {
+ number: match.number,
+ round: match.round,
+ stage: match.stage.toUpperCase() as 'PRACTICE' | 'RANKING' | 'TEST',
+ scheduled_time: new Date(match.scheduled_time),
+ division_id: req.divisionId
+ } as InsertableRobotGameMatch,
+ participants: Object.entries(match.tables).map(([tableId, tableData]) => ({
+ team_id: tableData.team_id || null,
+ table_id: tableId
+ })) as InsertableRobotGameMatchParticipant[]
+ }));
+
+ const testMatch = {
+ match: {
+ number: 0,
+ round: 0,
+ stage: 'TEST' as 'PRACTICE' | 'RANKING' | 'TEST',
+ scheduled_time: new Date(),
+ division_id: req.divisionId
+ } as InsertableRobotGameMatch,
+ participants: [] as InsertableRobotGameMatchParticipant[]
+ };
+
+ const allMatches = [testMatch, ...matchesWithParticipants];
+
+ await db.robotGameMatches.createMany(allMatches);
+
+ const scoresheets = [];
+
+ for (const { match, participants } of matchesWithParticipants) {
+ for (const participant of participants) {
+ if (participant.team_id) {
+ scoresheets.push({
+ divisionId: req.divisionId,
+ teamId: participant.team_id,
+ stage: match.stage as 'PRACTICE' | 'RANKING',
+ round: match.round,
+ status: 'empty' as const,
+ escalated: false
+ });
+ }
}
}
- }
- const updated = await db.divisions
- .byId(req.divisionId)
- .update({ has_schedule: true, schedule_settings });
+ await db.scoresheets.createMany(scoresheets);
- if (!updated) {
- res.status(404).json({ error: 'Division not found' });
- return;
+ res.status(200).json({ ok: true });
+ } catch (error) {
+ console.error('Error creating matches:', error);
+ res.status(500).json({ error: 'Failed to create matches' });
}
+ })
+);
+
+router.put(
+ '/settings',
+ asHandler(async (req, res) => {
+ try {
+ const { schedule_settings } = req.body;
+
+ if (schedule_settings !== undefined && schedule_settings !== null) {
+ const requiredFields = [
+ 'match_length',
+ 'practice_cycle_time',
+ 'ranking_cycle_time',
+ 'judging_session_length',
+ 'judging_session_cycle_time'
+ ];
+
+ for (const field of requiredFields) {
+ if (!(field in schedule_settings) || typeof schedule_settings[field] !== 'number') {
+ res.status(400).json({
+ error: `Invalid schedule_settings: ${field} must be a number`
+ });
+ return;
+ }
+ }
+ }
- res.status(200).json({ ok: true });
- } catch (error) {
- console.error('Error updating division has_schedule:', error);
- res.status(500).json({ error: 'Failed to update division has_schedule' });
- }
-});
-
-router.delete('/schedule', async (req: SchedulerRequest, res) => {
- try {
- await Promise.all([
- db.judgingSessions.byDivision(req.divisionId).deleteAll(),
- db.robotGameMatches.byDivision(req.divisionId).deleteAll(),
- db.scoresheets.byDivision(req.divisionId).deleteAll(),
- db.judgingDeliberations.byDivision(req.divisionId).deleteAll(),
- db.finalDeliberations.byDivision(req.divisionId).delete(),
- db.divisions.byId(req.divisionId).update({ has_schedule: false, schedule_settings: null })
- ]);
+ const updated = await db.divisions
+ .byId(req.divisionId)
+ .update({ has_schedule: true, schedule_settings });
- res.status(200).json({ ok: true });
- } catch (error) {
- console.error('Error deleting division schedule:', error);
- res.status(500).json({ error: 'Failed to delete division schedule' });
- }
-});
+ if (!updated) {
+ res.status(404).json({ error: 'Division not found' });
+ return;
+ }
+
+ res.status(200).json({ ok: true });
+ } catch (error) {
+ console.error('Error updating division has_schedule:', error);
+ res.status(500).json({ error: 'Failed to update division has_schedule' });
+ }
+ })
+);
+
+router.delete(
+ '/schedule',
+ asHandler(async (req, res) => {
+ try {
+ await Promise.all([
+ db.judgingSessions.byDivision(req.divisionId).deleteAll(),
+ db.robotGameMatches.byDivision(req.divisionId).deleteAll(),
+ db.scoresheets.byDivision(req.divisionId).deleteAll(),
+ db.judgingDeliberations.byDivision(req.divisionId).deleteAll(),
+ db.finalDeliberations.byDivision(req.divisionId).delete(),
+ db.divisions.byId(req.divisionId).update({ has_schedule: false, schedule_settings: null })
+ ]);
+
+ res.status(200).json({ ok: true });
+ } catch (error) {
+ console.error('Error deleting division schedule:', error);
+ res.status(500).json({ error: 'Failed to delete division schedule' });
+ }
+ })
+);
export default router;
diff --git a/apps/backend/src/routers/scheduler/middleware/auth.ts b/apps/backend/src/routers/scheduler/middleware/auth.ts
index 603147b8a..cab790f59 100644
--- a/apps/backend/src/routers/scheduler/middleware/auth.ts
+++ b/apps/backend/src/routers/scheduler/middleware/auth.ts
@@ -4,6 +4,10 @@ import { extractToken } from '../../../lib/security/auth';
const schedulerJwtSecret = process.env.SCHEDULER_JWT_SECRET;
+if (!schedulerJwtSecret) {
+ throw new Error('SCHEDULER_JWT_SECRET environment variable is required');
+}
+
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
try {
const token = extractToken(req);
diff --git a/apps/backend/src/types/express-handlers.ts b/apps/backend/src/types/express-handlers.ts
new file mode 100644
index 000000000..36b1e4609
--- /dev/null
+++ b/apps/backend/src/types/express-handlers.ts
@@ -0,0 +1,20 @@
+import { NextFunction, Request, RequestHandler, Response } from 'express';
+
+/**
+ * Casts a route handler with a narrowed request type to Express's RequestHandler.
+ * Middleware upstream is responsible for populating the narrowed fields.
+ */
+export function asHandler(
+ handler: (req: TReq, res: Response, next?: NextFunction) => void | Promise
+): RequestHandler {
+ return handler as RequestHandler;
+}
+
+/**
+ * Casts middleware with a narrowed request type to Express's RequestHandler.
+ */
+export function asMiddleware(
+ middleware: (req: TReq, res: Response, next: NextFunction) => void | Promise
+): RequestHandler {
+ return middleware as RequestHandler;
+}
diff --git a/apps/backend/tsconfig.app.json b/apps/backend/tsconfig.app.json
index 12487b442..6abdbcb48 100644
--- a/apps/backend/tsconfig.app.json
+++ b/apps/backend/tsconfig.app.json
@@ -4,6 +4,7 @@
"outDir": "../../dist/out-tsc",
"target": "es2017",
"module": "esnext",
+ "strict": true,
"types": ["node", "express"]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
diff --git a/apps/frontend/locale/he.json b/apps/frontend/locale/he.json
index 3e5171e3e..4efad86c3 100644
--- a/apps/frontend/locale/he.json
+++ b/apps/frontend/locale/he.json
@@ -1637,4 +1637,4 @@
"go-home": "חזרה לעמוד הבית",
"back": "חזור"
}
-}
\ No newline at end of file
+}
diff --git a/apps/frontend/src/app/[locale]/[event]/login/components/login-error-boundary.tsx b/apps/frontend/src/app/[locale]/[event]/login/components/login-error-boundary.tsx
index 558b2ab2c..79f2f112e 100644
--- a/apps/frontend/src/app/[locale]/[event]/login/components/login-error-boundary.tsx
+++ b/apps/frontend/src/app/[locale]/[event]/login/components/login-error-boundary.tsx
@@ -95,9 +95,10 @@ function LoginErrorFallback({ error, onReset }: LoginErrorFallbackProps) {
+ }}
+ >
{t('errors.description')}
@@ -119,10 +120,11 @@ function LoginErrorFallback({ error, onReset }: LoginErrorFallbackProps) {
+ }}
+ >
{t('errors.support-text')}
diff --git a/apps/frontend/src/app/[locale]/[event]/login/components/login-form.tsx b/apps/frontend/src/app/[locale]/[event]/login/components/login-form.tsx
index 82ea429a4..29612fd19 100644
--- a/apps/frontend/src/app/[locale]/[event]/login/components/login-form.tsx
+++ b/apps/frontend/src/app/[locale]/[event]/login/components/login-form.tsx
@@ -111,9 +111,11 @@ export function LoginForm({ recaptchaRequired }: LoginFormProps) {
return (
();
@@ -48,10 +48,11 @@ export function PasswordStep({disableLogin}: {disableLogin: boolean}) {
+ }}
+ >
{t('instructions.password')}
+ display: 'flex',
+ justifyContent: 'center',
+ width: '100%'
+ }}
+ >
+ }}
+ >
{t(`instructions.role-info-${roleInfoType}`)}
+ }}
+ >
{t('instructions.user')}
= ({ event, variant }) => {
{isLive && (
= ({ event, variant }) => {
borderRadius: 1,
fontSize: '0.875rem',
fontWeight: 600
- }}>
+ }}
+ >
= ({ event, variant }) => {
{/* Event Name */}
-
+
+ }}
+ >
{event.name}
{!event.official && (
@@ -153,16 +159,21 @@ export const EventCard: React.FC = ({ event, variant }) => {
{/* Event Details */}
-
+
+ color: 'text.secondary',
+ fontSize: '0.95rem'
+ }}
+ >
{new Date(event.startDate).toLocaleDateString(locale, {
weekday: 'long',
year: 'numeric',
@@ -172,16 +183,21 @@ export const EventCard: React.FC = ({ event, variant }) => {
-
+
+ color: 'text.secondary',
+ fontSize: '0.95rem'
+ }}
+ >
{event.region}
@@ -192,14 +208,18 @@ export const EventCard: React.FC = ({ event, variant }) => {
direction="row"
spacing={1}
sx={{
- alignItems: "center",
+ alignItems: 'center',
color: isLive ? 'error.main' : 'primary.main',
fontWeight: 600,
fontSize: '0.95rem'
- }}>
-
+ }}
+ >
+
{isLive ? t('view-event') : t('view-details')}
diff --git a/apps/frontend/src/app/[locale]/components/homepage/hero.tsx b/apps/frontend/src/app/[locale]/components/homepage/hero.tsx
index 2d6471b5f..a67211e74 100644
--- a/apps/frontend/src/app/[locale]/components/homepage/hero.tsx
+++ b/apps/frontend/src/app/[locale]/components/homepage/hero.tsx
@@ -12,40 +12,43 @@ export const Hero: React.FC = () => {
return (
+ }}
+ >
{/* Decorative circles */}
+ }}
+ />
+ }}
+ />
{/* Subtle grid lines for depth */}
{
backgroundSize: '60px 60px',
opacity: 0.5
- }} />
+ }}
+ />
{/* Clean gradient fade at bottom */}
+ }}
+ />
{
alignItems: { xs: 'center', md: 'flex-start' },
textAlign: { xs: 'center', md: 'left' },
maxWidth: 900
- }}>
+ }}
+ >
{/* Minimal badge */}
{
+ }}
+ >
{t('title')}
@@ -114,7 +121,7 @@ export const Hero: React.FC = () => {
{
'& i': {
fontStyle: 'italic'
}
- }}>
+ }}
+ >
{tags => t.rich('subtitle', tags)}
diff --git a/apps/frontend/src/app/[locale]/components/homepage/live-events-section.tsx b/apps/frontend/src/app/[locale]/components/homepage/live-events-section.tsx
index 3acfb8d2e..a623546ec 100644
--- a/apps/frontend/src/app/[locale]/components/homepage/live-events-section.tsx
+++ b/apps/frontend/src/app/[locale]/components/homepage/live-events-section.tsx
@@ -57,9 +57,10 @@ export const LiveEventsSection: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
{
+ }}
+ >
{t('live-events-title')}
-
+
{t('no-live-events')}
router.push('/events')} sx={{ mt: 3 }}>
@@ -101,9 +107,10 @@ export const LiveEventsSection: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
{
+ }}
+ >
{t('live-events-title')}
+ }}
+ >
{t('live-events-subtitle')}
diff --git a/apps/frontend/src/app/[locale]/components/homepage/upcoming-events-section.tsx b/apps/frontend/src/app/[locale]/components/homepage/upcoming-events-section.tsx
index 8fd7c8b2b..a566fed46 100644
--- a/apps/frontend/src/app/[locale]/components/homepage/upcoming-events-section.tsx
+++ b/apps/frontend/src/app/[locale]/components/homepage/upcoming-events-section.tsx
@@ -55,9 +55,10 @@ export const UpcomingEventsSection: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
{
+ }}
+ >
{t('upcoming-events-title')}
-
+
{t('no-upcoming-events')}
router.push('/events')} sx={{ mt: 3 }}>
@@ -94,9 +100,10 @@ export const UpcomingEventsSection: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
{
+ }}
+ >
{t('upcoming-events-title')}
+ }}
+ >
{t('upcoming-events-subtitle')}
diff --git a/apps/frontend/src/app/[locale]/components/time-sync-provider.tsx b/apps/frontend/src/app/[locale]/components/time-sync-provider.tsx
index aab72c003..1d3bc7957 100644
--- a/apps/frontend/src/app/[locale]/components/time-sync-provider.tsx
+++ b/apps/frontend/src/app/[locale]/components/time-sync-provider.tsx
@@ -21,20 +21,20 @@ interface TimeSyncProviderProps {
/**
* Provider component for server time synchronization
* Synchronizes client clock with server time using SNTP-based algorithm
- *
+ *
* Usage:
* ```tsx
*
*
*
* ```
- *
+ *
* Then use `useTimeSync()` hook to access the time offset in child components
*/
export const TimeSyncProvider: React.FC = ({
children,
serverUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/timesync`,
- syncInterval = 30 * 1000, // 30 seconds
+ syncInterval = 30 * 1000 // 30 seconds
}) => {
const [offset, setOffset] = useState(0);
const [isInitialized, setIsInitialized] = useState(false);
@@ -49,7 +49,7 @@ export const TimeSyncProvider: React.FC = ({
server: serverUrl,
interval: syncInterval,
timeout: 10000,
- delay: 1000,
+ delay: 1000
});
const handleOffsetChange = (newOffset: number) => {
@@ -90,7 +90,7 @@ export const TimeSyncProvider: React.FC = ({
return () => {
isMounted = false;
- cleanup?.then((fn) => fn?.());
+ cleanup?.then(fn => fn?.());
if (timesync) {
timesync.destroy();
}
diff --git a/apps/frontend/src/app/[locale]/events/page.tsx b/apps/frontend/src/app/[locale]/events/page.tsx
index 69a122074..99827cff7 100644
--- a/apps/frontend/src/app/[locale]/events/page.tsx
+++ b/apps/frontend/src/app/[locale]/events/page.tsx
@@ -43,9 +43,10 @@ export default function BrowseEventsPage() {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 2
- }}>
+ }}
+ >
} onClick={() => router.back()} variant="text">
{t('back')}
@@ -56,18 +57,20 @@ export default function BrowseEventsPage() {
variant="h3"
gutterBottom
sx={{
- fontWeight: "700",
+ fontWeight: '700',
fontSize: { xs: '2rem', md: '2.5rem' }
- }}>
+ }}
+ >
{t('title')}
+ }}
+ >
{tags => t.rich('description', tags)}
@@ -75,9 +78,13 @@ export default function BrowseEventsPage() {
{/* Events Grid */}
{loading ? (
- {t('loading')}
+
+ {t('loading')}
+
) : error ? (
@@ -86,17 +93,22 @@ export default function BrowseEventsPage() {
) : allEvents.length === 0 ? (
-
+
{t('no-events')}
+ }}
+ >
{t('no-events-description')}
router.push('/')}>
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/division-switcher.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/division-switcher.tsx
index f11a265b7..9c18d0bc9 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/division-switcher.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/division-switcher.tsx
@@ -143,9 +143,13 @@ export const DivisionSwitcher = () => {
flexShrink: 0
}}
/>
-
+
{division.name}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/navigation-list.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/navigation-list.tsx
index def91e493..414ad3fc0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/navigation-list.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/desktop/navigation-list.tsx
@@ -25,8 +25,9 @@ export const NavigationList: React.FC = ({ onItemClick }) =
spacing={4}
sx={{
mt: 2,
- alignItems: "center"
- }}>
+ alignItems: 'center'
+ }}
+ >
{items.map(item => (
= ({ onItemClick }) =
bgcolor: item.active ? lighten(theme.palette.primary.main, 0.8) : 'none',
width: 70,
height: 40,
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
color: item.active ? 'primary.main' : 'text.secondary',
transition:
@@ -67,7 +68,8 @@ export const NavigationList: React.FC = ({ onItemClick }) =
boxShadow: item.active ? theme.shadows[3] : 'none',
transform: 'translateY(0)'
- }}>
+ }}
+ >
{item.icon}
= ({ onItemClick }) =
mt: 1,
fontWeight: item.active ? 700 : 600,
transition: 'color 0.15s, font-weight 0.15s'
- }}>
+ }}
+ >
{t(item.label)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/app-bar.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/app-bar.tsx
index ec72a25a3..c1e89f5bf 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/app-bar.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/app-bar.tsx
@@ -69,10 +69,11 @@ export const MobileAppBar = () => {
+ }}
+ >
setMenuOpen(false)} />
setMenuOpen(false)} />
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/division-switcher.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/division-switcher.tsx
index 72d2c01bc..5caa408c7 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/division-switcher.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/division-switcher.tsx
@@ -131,9 +131,13 @@ export const DivisionSwitcher = () => {
flexShrink: 0
}}
/>
-
+
{division.name}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/navigation-list.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/navigation-list.tsx
index f9efd7a97..3250db921 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/navigation-list.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/navigation-list.tsx
@@ -62,7 +62,8 @@ export const NavigationList: React.FC = ({ onItemClick }) =
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
- }}>
+ }}
+ >
{item.icon}
= ({ onItemClick }) =
sx={{
fontWeight: item.active ? 700 : 600,
transition: 'all 0.15s ease-in-out'
- }}>
+ }}
+ >
{t(item.label)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/user-info-section.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/user-info-section.tsx
index f1c799fa7..8600f28d8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/user-info-section.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/mobile/user-info-section.tsx
@@ -23,9 +23,13 @@ export const UserInfoSection = () => {
>
{getRole(user.role)}
-
+
{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
- }}>
+ }}
+ >
{event.eventName}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/sound-test-dialog.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/sound-test-dialog.tsx
index b29e69f00..1e2cd9171 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/sound-test-dialog.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/sound-test-dialog.tsx
@@ -35,14 +35,18 @@ export const SoundTestDialog: React.FC = ({ open, setOpen
+ }}
+ >
{['start', 'change', 'end'].map(key => (
-
+
playSound(key as 'start' | 'change' | 'end')}
sx={{ width: 36, height: 36 }}
@@ -53,9 +57,10 @@ export const SoundTestDialog: React.FC = ({ open, setOpen
+ fontSize: '0.75rem',
+ textAlign: 'center'
+ }}
+ >
{t(`sounds.${key}`)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/team-info.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/team-info.tsx
index 9cf408b0c..3bcdbe8aa 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/team-info.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/components/team-info.tsx
@@ -44,13 +44,14 @@ export const TeamInfo: React.FC = ({ team, size, textAlign = 'lef
+ }}
+ >
{team.affiliation && `${team.affiliation}, `}
{team.city}
{team.region && (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/active-match-display.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/active-match-display.tsx
index ddbb76882..c8c475dc9 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/active-match-display.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/active-match-display.tsx
@@ -90,13 +90,17 @@ export function ActiveMatchDisplay() {
direction="row"
spacing={1}
sx={{
- alignItems: "center",
- flexWrap: "wrap"
- }}>
+ alignItems: 'center',
+ flexWrap: 'wrap'
+ }}
+ >
-
+
{t('running-match')}
) : (
-
+
-
+
{t('running-match')}
@@ -141,13 +152,20 @@ export function ActiveMatchDisplay() {
}}
>
-
+
-
+
{t('next-match')}
@@ -157,17 +175,19 @@ export function ActiveMatchDisplay() {
direction="row"
spacing={1}
sx={{
- alignItems: "center",
- flexWrap: "wrap",
+ alignItems: 'center',
+ flexWrap: 'wrap',
justifyContent: 'space-between'
- }}>
+ }}
+ >
+ alignItems: 'center',
+ flexWrap: 'wrap'
+ }}
+ >
+ }}
+ >
{children};
+ return (
+ {children}
+ );
}
export function useFieldHeadQueuer() {
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/field-schedule.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/field-schedule.tsx
index 94dcbe541..cb33daac8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/field-schedule.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/components/field-schedule.tsx
@@ -112,17 +112,21 @@ export function FieldSchedule() {
if (availableMatches.length === 0) {
return (
-
+
{t('no-matches')}
+ }}
+ >
{t('no-matches-description')}
@@ -133,9 +137,12 @@ export function FieldSchedule() {
{t('title')}
-
+
{t('subtitle')}
@@ -217,9 +224,12 @@ export function FieldSchedule() {
-
+
{dayjs(match.scheduledTime).format('HH:mm')}
@@ -234,9 +244,10 @@ export function FieldSchedule() {
+ alignItems: 'center',
+ justifyContent: 'center'
+ }}
+ >
{team ? (
) : (
-
+
—
)}
{match.called && team && participant && (
-
+
-
+
{t('no-sessions')}
+ }}
+ >
{t('no-sessions-description')}
@@ -149,9 +153,12 @@ export function JudgingSchedule({
{t('title')}
-
+
{t('subtitle')}
@@ -185,9 +192,12 @@ export function JudgingSchedule({
-
+
{dayjs(firstSession.scheduledTime).format('HH:mm')}
@@ -201,9 +211,10 @@ export function JudgingSchedule({
+ alignItems: 'center',
+ justifyContent: 'center'
+ }}
+ >
{team && (
-
+
-
+
{t('title')}
@@ -71,12 +78,16 @@ export function JudgingStatusTimer({
-
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+
{t('current-session', { number: currentSessionNumber })}
-
+
-
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+
{t('next-session', { number: currentSessionNumber + 1 })}
-
+
{t('teams-ready', { count: nextStats.queued })}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/graphql/query.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/graphql/query.ts
index b23a654e2..5666c19fd 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/graphql/query.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-head-queuer/graphql/query.ts
@@ -61,7 +61,7 @@ export const GET_HEAD_QUEUER_DATA: TypedDocumentNode = gql
export function parseHeadQueuerData(queryData: QueryData): HeadQueuerData {
const division = queryData.division;
-
+
if (!division) {
return {
matches: [],
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/field-schedule-view.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/field-schedule-view.tsx
index bcaf2264e..491fc5651 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/field-schedule-view.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/field-schedule-view.tsx
@@ -63,9 +63,12 @@ export function FieldScheduleView({ data, loading }: FieldScheduleViewProps) {
if (upcomingMatches.length === 0) {
return (
-
+
{t('no-matches')}
@@ -75,14 +78,20 @@ export function FieldScheduleView({ data, loading }: FieldScheduleViewProps) {
return (
-
+
{t('title')}
-
+
{t('subtitle')}
@@ -166,9 +175,12 @@ export function FieldScheduleView({ data, loading }: FieldScheduleViewProps) {
/>
-
+
{dayjs(match.scheduledTime).format('HH:mm')}
@@ -187,9 +199,12 @@ export function FieldScheduleView({ data, loading }: FieldScheduleViewProps) {
sx={{ fontWeight: 600, minWidth: 50 }}
/>
) : (
-
+
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/pit-map-view.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/pit-map-view.tsx
index fc95c9590..be6d68b33 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/pit-map-view.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/pit-map-view.tsx
@@ -27,9 +27,12 @@ export function PitMapView() {
if (error || !pitMapUrl) {
return (
-
+
{error ? t('error') : t('no-map')}
@@ -47,14 +50,20 @@ export function PitMapView() {
}}
>
-
+
{t('title')}
-
+
{t('subtitle')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/team-queue-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/team-queue-card.tsx
index dbba1d69f..91f90a9d4 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/team-queue-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/components/team-queue-card.tsx
@@ -64,18 +64,29 @@ export function TeamQueueCard({
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "space-between"
- }}>
-
-
-
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+
+
+
#{teamNumber}
{isInJudging && (
@@ -84,24 +95,31 @@ export function TeamQueueCard({
)}
-
+
{teamName}
-
+
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ justifyContent: 'flex-end'
+ }}
+ >
-
+
{timeInfo.formattedTime} (
{timeInfo.isPast
? t('time-ago', { minutes: timeInfo.diffMinutes })
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/query.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/query.ts
index a279b6ee4..517e80259 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/query.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/query.ts
@@ -71,7 +71,7 @@ export const GET_FIELD_QUEUER_DATA: TypedDocumentNode = gq
export function parseFieldQueuerData(queryData: QueryData): FieldQueuerData {
const division = queryData.division;
-
+
if (!division) {
return {
matches: [],
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/subscriptions/match-call-updated.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/subscriptions/match-call-updated.ts
index 741113423..36777ba86 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/subscriptions/match-call-updated.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/graphql/subscriptions/match-call-updated.ts
@@ -15,17 +15,15 @@ export interface MatchUpdatedData {
matchUpdated: MatchUpdatedEvent;
}
-export const MATCH_UPDATED_SUBSCRIPTION: TypedDocumentNode<
- MatchUpdatedData,
- SubscriptionVars
-> = gql`
- subscription MatchUpdated($divisionId: String!) {
- matchUpdated(divisionId: $divisionId) {
- id
- called
+export const MATCH_UPDATED_SUBSCRIPTION: TypedDocumentNode =
+ gql`
+ subscription MatchUpdated($divisionId: String!) {
+ matchUpdated(divisionId: $divisionId) {
+ id
+ called
+ }
}
- }
-`;
+ `;
export function createMatchCallUpdatedSubscription(divisionId: string) {
return {
@@ -34,7 +32,7 @@ export function createMatchCallUpdatedSubscription(divisionId: string) {
updateQuery: (prev: QueryData, subscriptionData: { data?: unknown }) => {
const data = subscriptionData.data as MatchUpdatedData | undefined;
if (!data || !prev.division) return prev;
-
+
const updatedMatchId = data.matchUpdated.id;
const updatedCalled = data.matchUpdated.called;
@@ -45,9 +43,7 @@ export function createMatchCallUpdatedSubscription(divisionId: string) {
field: {
...prev.division.field,
matches: prev.division.field.matches.map(match =>
- match.id === updatedMatchId
- ? { ...match, called: updatedCalled }
- : match
+ match.id === updatedMatchId ? { ...match, called: updatedCalled } : match
)
}
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/page.tsx
index 9d3450b0a..5db8dc6ea 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/field-queuer/page.tsx
@@ -111,9 +111,13 @@ function FieldQueuerContent() {
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
-
+
{t('filter-by-table')}
@@ -143,17 +147,21 @@ function FieldQueuerContent() {
{!loading && filteredTeams.length === 0 && (
-
+
{t('no-teams')}
+ }}
+ >
{t('no-teams-description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/desktop-schedule/match-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/desktop-schedule/match-row.tsx
index 39436e45b..e9e5cdca1 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/desktop-schedule/match-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/desktop-schedule/match-row.tsx
@@ -64,7 +64,8 @@ export function MatchRow({
sx={{
fontWeight: 600,
fontSize: '1.25rem'
- }}>
+ }}
+ >
#{match.number}
@@ -72,10 +73,11 @@ export function MatchRow({
+ }}
+ >
{scheduledTime}
@@ -87,9 +89,10 @@ export function MatchRow({
+ }}
+ >
-
@@ -151,10 +154,11 @@ export function MatchRow({
direction="row"
spacing={0.5}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
width: '100%'
- }}>
+ }}
+ >
{showIcon &&
(isReady ? (
+ }}
+ >
#{participant.team.number}
+ }}
+ >
{participant.team.name}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/escalated-scoresheets-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/escalated-scoresheets-panel.tsx
index bc082b54f..856fc4321 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/escalated-scoresheets-panel.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/escalated-scoresheets-panel.tsx
@@ -27,9 +27,12 @@ export function EscalatedScoresheetsPanel() {
}
title={
-
+
{t('escalated-panel.title', { count: escalatedScoresheets.length })}
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/filters.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/filters.tsx
index c35d2ac5a..bc4aaea0d 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/filters.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/filters.tsx
@@ -42,19 +42,21 @@ export function Filters() {
spacing={2}
useFlexGap
sx={{
- alignItems: "center",
- flexWrap: "wrap",
- justifyContent: "space-between"
- }}>
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ justifyContent: 'space-between'
+ }}
+ >
+ }}
+ >
+ }}
+ >
#{match.number}
+ }}
+ >
{participant.table.name}
+ }}
+ >
{participant.team!.name} #{participant.team!.number}
{participant.team!.region && (
<>
@@ -207,11 +210,12 @@ function MatchCard({
+ }}
+ >
{participant.team!.affiliation && `${participant.team!.affiliation}, `}
{participant.team!.city}
@@ -275,20 +279,22 @@ function MatchCard({
variant="body2"
sx={{
fontWeight: 700,
- color: "text.primary",
+ color: 'text.primary',
fontSize: '1.25rem',
wordBreak: 'break-word'
- }}>
+ }}
+ >
{participant.table.name}
+ }}
+ >
{participant.team!.name}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/round-schedule.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/round-schedule.tsx
index b8624ccdc..cb4883c5b 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/round-schedule.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/head-referee/components/round-schedule.tsx
@@ -62,8 +62,9 @@ export function RoundSchedule({ roundGroup, defaultExpanded = true }: RoundSched
variant="h6"
sx={{
fontWeight: 600,
- color: "text.primary"
- }}>
+ color: 'text.primary'
+ }}
+ >
{title}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/category-deliberation-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/category-deliberation-card.tsx
index 449a085a1..7850955b7 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/category-deliberation-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/category-deliberation-card.tsx
@@ -77,7 +77,8 @@ export function CategoryDeliberationCard({
sx={{
fontWeight: 600,
mb: 2
- }}>
+ }}
+ >
{label}
@@ -85,18 +86,20 @@ export function CategoryDeliberationCard({
spacing={2}
direction="row"
sx={{
- justifyContent: "space-between",
- alignItems: "center",
+ justifyContent: 'space-between',
+ alignItems: 'center',
mb: 2
- }}>
+ }}
+ >
+ }}
+ >
{t('status')}
+ }}
+ >
{t('start-time')}
+ fontFamily: 'monospace'
+ }}
+ >
{deliberation.startTime ? dayjs(deliberation.startTime).format('HH:mm') : '—'}
@@ -132,10 +137,11 @@ export function CategoryDeliberationCard({
+ }}
+ >
{t('picklist')}
{deliberation ? (
@@ -143,9 +149,12 @@ export function CategoryDeliberationCard({
{deliberation.picklist.length} / {desiredPicklistLength}
) : (
-
+
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/deliberation-status-section.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/deliberation-status-section.tsx
index 3c2e8461e..a0651a881 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/deliberation-status-section.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/deliberation/deliberation-status-section.tsx
@@ -39,10 +39,11 @@ export function DeliberationStatusSection() {
+ }}
+ >
{categoriesWithStatuses.map(({ category, deliberation }) => (
+ }}
+ >
{t('stage')}
+ }}
+ >
{t('status')}
+ }}
+ >
{t('start-time')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards-section.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards-section.tsx
index 670be21ca..0396f85ca 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards-section.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards-section.tsx
@@ -175,9 +175,12 @@ export function PersonalAwardsSection() {
borderLeft: `4px solid ${theme.palette.info.main}`
}}
>
-
+
{t('no-awards')}
@@ -241,10 +244,11 @@ export function PersonalAwardsSection() {
component="span"
variant="caption"
sx={{
- color: "text.secondary",
+ color: 'text.secondary',
display: 'block',
mt: 0.25
- }}>
+ }}
+ >
({t('optional')})
)}
@@ -262,7 +266,7 @@ export function PersonalAwardsSection() {
{isAssigned ? (
// Assigned award: show winner name as text
- (
{assignedWinner}
- )
+
) : (
// Unassigned award: show input field and button on same line
- (
+
- )
+
)}
);
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards/assign-award-confirmation-dialog.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards/assign-award-confirmation-dialog.tsx
index f46138938..b635efd1a 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards/assign-award-confirmation-dialog.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/personal-awards/assign-award-confirmation-dialog.tsx
@@ -49,9 +49,12 @@ export function AssignAwardConfirmationDialog({
-
+
{t('confirm-assignment-message')}
-
+
{t('award-label')}
@@ -74,9 +80,12 @@ export function AssignAwardConfirmationDialog({
-
+
{t('winner-label')}
@@ -85,9 +94,12 @@ export function AssignAwardConfirmationDialog({
-
+
{t('confirm-assignment-warning')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/rubric-status-glossary.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/rubric-status-glossary.tsx
index 73398ac6d..e8d6d3b13 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/rubric-status-glossary.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/rubric-status-glossary.tsx
@@ -111,9 +111,10 @@ export const RubricStatusGlossary: React.FC = () => {
direction="row"
spacing={1.5}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
py: 0.5
- }}>
+ }}
+ >
{
+ }}
+ >
{range(JUDGING_CATEGORIES.length).map(i => (
{
return (
+ }}
+ >
{Object.entries(stats).map(([category, stat]) => {
const color = getRubricColor(category as JudgingCategory);
@@ -86,7 +88,8 @@ export const RubricStatusSummary = () => {
sx={{
fontWeight: 600,
mb: 2
- }}>
+ }}
+ >
{label}
@@ -94,10 +97,11 @@ export const RubricStatusSummary = () => {
+ }}
+ >
{t('status.empty')}
@@ -110,10 +114,11 @@ export const RubricStatusSummary = () => {
+ }}
+ >
{t('status.draft')}
@@ -126,10 +131,11 @@ export const RubricStatusSummary = () => {
+ }}
+ >
{t('status.completed')}
@@ -138,7 +144,8 @@ export const RubricStatusSummary = () => {
sx={{
fontWeight: 600,
color: completed + locked + approved === total ? '#4caf50' : 'inherit'
- }}>
+ }}
+ >
{completed + locked + approved}
@@ -148,16 +155,18 @@ export const RubricStatusSummary = () => {
+ }}
+ >
+ }}
+ >
{t('status.approved')}
{
fontWeight: 600,
mt: 2,
mb: 1
- }}>
+ }}
+ >
{total > 0 ? Math.round((approved / total) * 100) : 0}%
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/status-filter-selector.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/status-filter-selector.tsx
index a78a74bf5..906bafe1f 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/status-filter-selector.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge-advisor/components/status-filter-selector.tsx
@@ -82,9 +82,13 @@ export const StatusFilterSelector: React.FC = ({
borderColor: 'rgba(0, 0, 0, 0.25)'
}}
>
- {displayLabel}
+
+ {displayLabel}
+
-
+
=
direction="row"
spacing={1}
sx={{
- flexWrap: "wrap",
- justifyContent: "center"
- }}>
+ flexWrap: 'wrap',
+ justifyContent: 'center'
+ }}
+ >
•
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge/components/timer/stage-timeline.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge/components/timer/stage-timeline.tsx
index 565fcc15b..426ca35e9 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge/components/timer/stage-timeline.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judge/components/timer/stage-timeline.tsx
@@ -47,26 +47,29 @@ export const StageTimeline = ({ timerState }: StageTimelineProps) => {
+ }}
+ >
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+ }}
+ >
{
{t('timer.minutes', { count: stage.duration / 60 })}
-
+
-
+
-
+
{t('current-sessions')}
@@ -126,12 +133,13 @@ export function CurrentSessionsDisplay({
direction="row"
spacing={1}
sx={{
- alignItems: "center",
- flexWrap: "wrap",
+ alignItems: 'center',
+ flexWrap: 'wrap',
p: 1,
borderRadius: 1,
bgcolor: 'rgba(255,255,255,0.08)'
- }}>
+ }}
+ >
-
+
-
+
{t('upcoming-sessions')}
@@ -203,17 +218,19 @@ export function CurrentSessionsDisplay({
direction="row"
spacing={1}
sx={{
- alignItems: "center",
- flexWrap: "wrap",
+ alignItems: 'center',
+ flexWrap: 'wrap',
justifyContent: 'space-between'
- }}>
+ }}
+ >
+ alignItems: 'center',
+ flexWrap: 'wrap'
+ }}
+ >
+ }}
+ >
-
+
{t('no-sessions')}
+ }}
+ >
{t('no-sessions-description')}
@@ -128,9 +132,12 @@ export function JudgingSchedule({ divisionId, sessions, rooms, loading }: Judgin
{t('title')}
-
+
{t('subtitle')}
@@ -146,21 +153,33 @@ export function JudgingSchedule({ divisionId, sessions, rooms, loading }: Judgin
- {t('time')}
+
+ {t('time')}
+
{rooms.map(room => (
- {room.name}
+
+ {room.name}
+
))}
- {t('actions')}
+
+ {t('actions')}
+
@@ -175,9 +194,10 @@ export function JudgingSchedule({ divisionId, sessions, rooms, loading }: Judgin
+ }}
+ >
{currentTime
.set('hour', new Date(time).getHours())
.set('minute', new Date(time).getMinutes())
@@ -189,9 +209,13 @@ export function JudgingSchedule({ divisionId, sessions, rooms, loading }: Judgin
if (!session) {
return (
- -
+
+ -
+
);
}
@@ -218,9 +242,10 @@ export function JudgingSchedule({ divisionId, sessions, rooms, loading }: Judgin
) : (
+ }}
+ >
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/judging-schedule-view.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/judging-schedule-view.tsx
index 95b506393..5a88017b7 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/judging-schedule-view.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/judging-schedule-view.tsx
@@ -66,17 +66,21 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
if (timeSlots.length === 0) {
return (
-
+
{t('no-sessions')}
+ }}
+ >
{t('no-sessions-description')}
@@ -87,9 +91,12 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
{t('title')}
-
+
{t('subtitle')}
@@ -105,15 +112,23 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
- {t('time')}
+
+ {t('time')}
+
{rooms.map(room => (
- {room.name}
+
+ {room.name}
+
))}
@@ -124,9 +139,10 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
+ }}
+ >
{currentTime
.set('hour', new Date(time).getHours())
.set('minute', new Date(time).getMinutes())
@@ -138,9 +154,13 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
if (!session) {
return (
- -
+
+ -
+
);
}
@@ -167,9 +187,10 @@ export function JudgingScheduleView({ data, loading }: JudgingScheduleViewProps)
) : (
+ }}
+ >
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/pit-map-view.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/pit-map-view.tsx
index 9020c3cad..7f8b39b0c 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/pit-map-view.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/pit-map-view.tsx
@@ -9,18 +9,27 @@ export function PitMapView() {
return (
-
+
-
+
{t('title')}
-
+
{t('description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/team-queue-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/team-queue-card.tsx
index 5edb2cd73..b9b55d00b 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/team-queue-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/components/team-queue-card.tsx
@@ -65,18 +65,29 @@ export function TeamQueueCard({
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "space-between"
- }}>
-
-
-
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+
+
+
#{teamNumber}
{isInMatch && (
@@ -85,24 +96,31 @@ export function TeamQueueCard({
)}
-
+
{teamName}
-
+
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ justifyContent: 'flex-end'
+ }}
+ >
-
+
{timeInfo.formattedTime} (
{timeInfo.isPast
? t('time-ago', { minutes: timeInfo.diffMinutes })
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/page.tsx
index 234708b86..b36123dea 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/judging-queuer/page.tsx
@@ -192,9 +192,13 @@ export default function JudgingQueuerPage() {
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
-
+
{t('filter-by-room')}
@@ -225,17 +229,21 @@ export default function JudgingQueuerPage() {
{!loading && filteredTeams.length === 0 && (
-
+
{t('no-teams')}
+ }}
+ >
{t('no-teams-description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/layout.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/layout.tsx
index 946fc8a91..75b3b4418 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/layout.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/layout.tsx
@@ -22,7 +22,8 @@ export default function VolunteerDashboardLayout({ children }: { children: React
flexGrow: 1,
backgroundColor: '#fafafa',
mt: { xs: 8, lg: 0 }
- }}>
+ }}
+ >
{
direction="row"
spacing={1.5}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
py: 0.5
- }}>
+ }}
+ >
{
+ }}
+ >
{t('status.approved')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/session-filters.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/session-filters.tsx
index 48c0ce61d..f6f6b1e37 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/session-filters.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/session-filters.tsx
@@ -97,9 +97,10 @@ export const SessionFilters: React.FC = () => {
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
{t('filter.results')}: {filteredSessions.length}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/status-filter-selector.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/status-filter-selector.tsx
index 4925e026c..f3f78d735 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/status-filter-selector.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/lead-judge/components/status-filter-selector.tsx
@@ -79,9 +79,13 @@ export const StatusFilterSelector: React.FC = ({
borderColor: 'rgba(0, 0, 0, 0.25)'
}}
>
- {displayLabel}
+
+ {displayLabel}
+
{/* Awards Placeholder Skeleton */}
-
+
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/mc/components/nav-buttons.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/mc/components/nav-buttons.tsx
index 982c939b7..2e50fe974 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/mc/components/nav-buttons.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/mc/components/nav-buttons.tsx
@@ -20,11 +20,12 @@ export const NavButtons: React.FC = ({ current, total, onPrevio
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
pt: 2,
pb: 1
- }}>
+ }}
+ >
{mode === 'matches' && (
-
+
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/pit-admin/components/arrivals-stats.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/pit-admin/components/arrivals-stats.tsx
index 253ec3981..663dd07ee 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/pit-admin/components/arrivals-stats.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/pit-admin/components/arrivals-stats.tsx
@@ -74,13 +74,18 @@ export function ArrivalsStats({ teams, loading = false }: ArrivalsStatsProps) {
direction={{ xs: 'column', md: 'row' }}
spacing={2}
sx={{
- alignItems: "flex-start",
- justifyContent: "space-between"
- }}>
+ alignItems: 'flex-start',
+ justifyContent: 'space-between'
+ }}
+ >
-
+
{t('total-teams')}
@@ -102,9 +107,13 @@ export function ArrivalsStats({ teams, loading = false }: ArrivalsStatsProps) {
-
+
{t('arrived')}
@@ -150,9 +159,12 @@ export function ArrivalsStats({ teams, loading = false }: ArrivalsStatsProps) {
}}
/>
-
+
{stats.pending > 0 && t('teams-pending', { count: stats.pending })}
{stats.pending === 0 && t('all-arrived')}
@@ -180,13 +192,20 @@ export function ArrivalsStats({ teams, loading = false }: ArrivalsStatsProps) {
border: '1px solid rgba(255, 255, 255, 0.2)'
}}
>
-
+
-
+
{t('waiting-for', { count: stats.missingTeams.length })}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/inspection-timer.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/inspection-timer.tsx
index dc459778e..2bf447b98 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/inspection-timer.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/inspection-timer.tsx
@@ -44,16 +44,26 @@ export const InspectionTimer: React.FC = ({ inspectionStar
borderRadius: 2
}}
>
-
-
+
+
-
+
{t('inspection-timer')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/match-timer.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/match-timer.tsx
index 522ea1a44..86dcbc510 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/match-timer.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/match-timer.tsx
@@ -142,9 +142,12 @@ export const RefereeMatchTimer = () => {
{participant.team.name} #{participant.team.number}
-
+
{participant.team.affiliation}, {participant.team.city}
@@ -164,9 +167,10 @@ export const RefereeMatchTimer = () => {
+ color: 'text.secondary',
+ textAlign: 'center'
+ }}
+ >
{t('team-not-available')}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/no-match.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/no-match.tsx
index 0ad2340af..136dfabeb 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/no-match.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/no-match.tsx
@@ -22,9 +22,12 @@ export function RefereeNoMatch() {
-
+
-
+
{t('no-match-instructions')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/schedule.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/schedule.tsx
index a04b91a6d..f14957686 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/schedule.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/referee/components/schedule.tsx
@@ -59,15 +59,21 @@ export function RefereeSchedule() {
}}
>
-
+
{getStage(match.stage)} #{match.number}
-
+
{t('round')} {match.round}
@@ -78,16 +84,23 @@ export function RefereeSchedule() {
-
+
{match.participants.map(p =>
p.team ? (
) : (
-
+
-
)
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/award-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/award-card.tsx
index 702cf2136..74d7da265 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/award-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/award-card.tsx
@@ -66,7 +66,8 @@ export function AwardCard({ name, awardList }: AwardCardProps) {
sx={{
fontWeight: 600,
flexGrow: 1
- }}>
+ }}
+ >
{localizedName}
@@ -96,9 +97,10 @@ export function AwardCard({ name, awardList }: AwardCardProps) {
+ }}
+ >
{description}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/empty-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/empty-state.tsx
index 5122b11b6..f4c87ff38 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/empty-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/empty-state.tsx
@@ -8,9 +8,12 @@ export function EmptyState() {
return (
-
+
{t('no-awards')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/error-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/error-state.tsx
index 9e4b92b2b..dbd8b03ca 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/error-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/error-state.tsx
@@ -8,9 +8,12 @@ export function ErrorState() {
return (
-
+
{t('error-loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/loading-state.tsx
index aca4adb82..f40df66f0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/components/loading-state.tsx
@@ -8,9 +8,13 @@ export function LoadingState() {
return (
- {t('loading')}
+
+ {t('loading')}
+
);
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/page.tsx
index 9a5d38927..7c1428301 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/awards-list/page.tsx
@@ -63,9 +63,12 @@ export default function AwardsListPage() {
}}
>
-
+
{t('description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/agenda-event-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/agenda-event-card.tsx
index a34f8869f..71ccc0eec 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/agenda-event-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/agenda-event-card.tsx
@@ -62,47 +62,65 @@ export function AgendaEventCard({ event }: AgendaEventCardProps) {
-
+
{event.title}
-
+ }}
+ >
+
{formattedTime}
-
+
•
-
+
{t('duration-minutes', { count: durationMinutes })}
{event.location && (
<>
-
+
•
+ }}
+ >
{event.location}
>
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/empty-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/empty-state.tsx
index 6986628a3..2ef0c1a39 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/empty-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/empty-state.tsx
@@ -10,9 +10,12 @@ export function EmptyState() {
return (
-
+
{t('no-events')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/error-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/error-state.tsx
index ed8c06b97..2ead51486 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/error-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/error-state.tsx
@@ -10,9 +10,12 @@ export function ErrorState() {
return (
-
+
{t('error-loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/loading-state.tsx
index 08ba6c36c..974af31be 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/event-agenda/components/loading-state.tsx
@@ -9,9 +9,12 @@ export function LoadingState() {
return (
-
+
{t('loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/agenda-event-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/agenda-event-row.tsx
index cd6a483d0..fdcdb0c89 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/agenda-event-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/agenda-event-row.tsx
@@ -32,10 +32,11 @@ export const AgendaEventRow: React.FC = ({ event, tableCoun
+ }}
+ >
{startTime.format('HH:mm')} - {endTime.format('HH:mm')}
@@ -49,7 +50,8 @@ export const AgendaEventRow: React.FC = ({ event, tableCoun
alignItems: 'center',
justifyContent: 'center',
gap: 1
- }}>
+ }}
+ >
{event.title}
-
+
{t('empty-state')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/loading-state.tsx
index f2f14e000..49df68cca 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/loading-state.tsx
@@ -9,9 +9,12 @@ export function LoadingState() {
return (
-
+
{t('loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/match-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/match-row.tsx
index e434670a3..3470282c2 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/match-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/match-row.tsx
@@ -40,30 +40,33 @@ export const MatchRow: React.FC = ({ match, tables, teams }) => {
+ }}
+ >
{match.number}
+ }}
+ >
{startTime.format('HH:mm')}
+ }}
+ >
{endTime.format('HH:mm')}
@@ -102,9 +105,13 @@ export const MatchRow: React.FC = ({ match, tables, teams }) => {
) : (
- -
+
+ -
+
)}
);
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/round-schedule.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/round-schedule.tsx
index 0ef794daa..dd2850714 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/round-schedule.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/components/round-schedule.tsx
@@ -51,7 +51,8 @@ export const RoundSchedule: React.FC = ({ matches, tables, t
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.875rem' : '1rem'
- }}>
+ }}
+ >
{roundTitle}
@@ -62,7 +63,8 @@ export const RoundSchedule: React.FC = ({ matches, tables, t
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('columns.match')}
@@ -71,7 +73,8 @@ export const RoundSchedule: React.FC = ({ matches, tables, t
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('columns.start-time')}
@@ -80,7 +83,8 @@ export const RoundSchedule: React.FC = ({ matches, tables, t
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('columns.end-time')}
@@ -90,7 +94,8 @@ export const RoundSchedule: React.FC = ({ matches, tables, t
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{table.name}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/graphql/query.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/graphql/query.ts
index b813b9891..04d51198c 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/graphql/query.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-schedule/graphql/query.ts
@@ -135,24 +135,31 @@ export function parseFieldScheduleData(data: QueryData) {
const roundRows: typeof rows = [];
// Get time boundaries for this round
- const lastMatchTime = dayjs(currentRoundMatches[currentRoundMatches.length - 1].scheduledTime);
-
+ const lastMatchTime = dayjs(
+ currentRoundMatches[currentRoundMatches.length - 1].scheduledTime
+ );
+
// Get the start boundary (before first match of this round)
- const startBoundary = roundIndex > 0
- ? dayjs(roundMatches[sortedRoundKeys[roundIndex - 1]][roundMatches[sortedRoundKeys[roundIndex - 1]].length - 1].scheduledTime)
- : dayjs(0);
-
+ const startBoundary =
+ roundIndex > 0
+ ? dayjs(
+ roundMatches[sortedRoundKeys[roundIndex - 1]][
+ roundMatches[sortedRoundKeys[roundIndex - 1]].length - 1
+ ].scheduledTime
+ )
+ : dayjs(0);
+
// Get the end boundary (before first match of next round, or infinity for last round)
- const endBoundary = roundIndex < sortedRoundKeys.length - 1
- ? dayjs(roundMatches[sortedRoundKeys[roundIndex + 1]][0].scheduledTime)
- : dayjs('9999-12-31');
+ const endBoundary =
+ roundIndex < sortedRoundKeys.length - 1
+ ? dayjs(roundMatches[sortedRoundKeys[roundIndex + 1]][0].scheduledTime)
+ : dayjs('9999-12-31');
// Add matches and events for this round in chronological order
currentRoundMatches.forEach((match, matchIndex) => {
const matchTime = dayjs(match.scheduledTime);
- const previousMatchTime = matchIndex > 0
- ? dayjs(currentRoundMatches[matchIndex - 1].scheduledTime)
- : startBoundary;
+ const previousMatchTime =
+ matchIndex > 0 ? dayjs(currentRoundMatches[matchIndex - 1].scheduledTime) : startBoundary;
// Add agenda events that fall between previous match and this match
agenda.forEach(event => {
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/match-countdown.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/match-countdown.tsx
index 03df5a5e6..0646bc117 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/match-countdown.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/match-countdown.tsx
@@ -62,10 +62,11 @@ export function MatchCountdown({
+ }}
+ >
{t('countdown.no-match')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/next-match-panel.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/next-match-panel.tsx
index 2d7102639..2e005c2a4 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/next-match-panel.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-status/components/next-match-panel.tsx
@@ -56,14 +56,21 @@ export function NextMatchPanel({ match }: NextMatchPanelProps) {
return (
-
+
{t('next-match.title')}
- {t('next-match.no-match')}
+
+ {t('next-match.no-match')}
+
);
@@ -102,25 +109,28 @@ export function NextMatchPanel({ match }: NextMatchPanelProps) {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "space-between",
- flexWrap: "wrap"
- }}>
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ flexWrap: 'wrap'
+ }}
+ >
+ }}
+ >
{getStage(match.stage)} #{match.number}
+ }}
+ >
{currentTime
.set('hour', dayjs(match.scheduledTime).hour())
.set('minute', dayjs(match.scheduledTime).minute())
@@ -128,15 +138,20 @@ export function NextMatchPanel({ match }: NextMatchPanelProps) {
-
+
+ }}
+ >
{t('next-match.tables-ready')}:
+ }}
+ >
+ }}
+ >
{participant.table.name}:
-
+
{t('upcoming-matches.title')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/loading-state.tsx
index b74c799f8..671e702d0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/loading-state.tsx
@@ -16,9 +16,12 @@ export function LoadingState() {
minHeight: 'calc(100vh - 200px)'
}}
>
-
+
{t('loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/match-info.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/match-info.tsx
index c0d40ab86..d9dff5e14 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/match-info.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/field-timer/components/match-info.tsx
@@ -44,7 +44,8 @@ export function MatchInfo({ match, isDesktop }: MatchInfoProps) {
fontWeight: 700,
color: theme => theme.palette.text.primary,
letterSpacing: '0.5px'
- }}>
+ }}
+ >
{getMatchLabel()}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/empty-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/empty-state.tsx
index 5bdcac2d3..83e09f2dc 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/empty-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/empty-state.tsx
@@ -10,9 +10,12 @@ export function EmptyState() {
return (
-
+
{t('no-sessions')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/error-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/error-state.tsx
index 3959c6a4a..4b750b96a 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/error-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/error-state.tsx
@@ -10,9 +10,12 @@ export function ErrorState() {
return (
-
+
{t('error-loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/loading-state.tsx
index d2048d8c9..448a97386 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/loading-state.tsx
@@ -9,9 +9,12 @@ export function LoadingState() {
return (
-
+
{t('loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/schedule-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/schedule-table.tsx
index 2fb4296c8..67f796b2d 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/schedule-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-schedule/components/schedule-table.tsx
@@ -58,7 +58,8 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('table.start-time')}
@@ -67,7 +68,8 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('table.end-time')}
@@ -77,7 +79,8 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{room.name}
@@ -104,20 +107,22 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
+ }}
+ >
{dayjs(row.time).format('HH:mm')}
+ }}
+ >
{endTime.format('HH:mm')}
@@ -131,7 +136,8 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
alignItems: 'center',
justifyContent: 'center',
gap: 1
- }}>
+ }}
+ >
{event.title}
+ }}
+ >
{sessionTime.format('HH:mm')}
+ }}
+ >
{sessionEndTime.format('HH:mm')}
@@ -219,9 +227,10 @@ export function ScheduleTable({ rooms, rows, sessionLength }: ScheduleTableProps
) : (
+ }}
+ >
-
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-mobile.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-mobile.tsx
index 296a0521a..e5b882239 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-mobile.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-mobile.tsx
@@ -43,9 +43,12 @@ export const JudgingStatusMobile: React.FC = () => {
if (currentSessions.length === 0 && nextSessions.length === 0) {
return (
-
+
{t('empty-state.message')}
@@ -61,9 +64,12 @@ export const JudgingStatusMobile: React.FC = () => {
{t('table.current-round')}
-
+
{dayjs(currentSessions[0].scheduledTime).format('HH:mm')}
@@ -93,9 +99,12 @@ export const JudgingStatusMobile: React.FC = () => {
{t('table.next-round')}
-
+
{dayjs(nextSessions[0].scheduledTime).format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-table.tsx
index 46685f9b4..86d5ec91b 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/judging-status-table.tsx
@@ -69,9 +69,12 @@ export const JudgingStatusTable: React.FC = () => {
if (currentSessions.length === 0 && nextSessions.length === 0) {
return (
-
+
{t('empty-state.message')}
@@ -115,9 +118,12 @@ export const JudgingStatusTable: React.FC = () => {
{t('table.current-round')}
-
+
{dayjs(currentSessions[0].scheduledTime).format('HH:mm')}
@@ -142,9 +148,12 @@ export const JudgingStatusTable: React.FC = () => {
{t('table.next-round')}
-
+
{dayjs(nextSessions[0].scheduledTime).format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/next-session-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/next-session-row.tsx
index 9e8bf696a..c9d385baa 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/next-session-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/next-session-row.tsx
@@ -20,11 +20,12 @@ export const NextSessionRow: React.FC = ({ session }) => {
+ }}
+ >
{!team.arrived && (
= ({ session }) => {
)}
) : (
-
+
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-card.tsx
index 013d7cff1..1f075bc58 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-card.tsx
@@ -39,9 +39,10 @@ export const SessionCard: React.FC = ({
+ }}
+ >
{t('table.room', { name: room.name })}
@@ -55,9 +56,10 @@ export const SessionCard: React.FC = ({
direction="row"
spacing={0.5}
sx={{
- alignItems: "center",
- flexWrap: "wrap"
- }}>
+ alignItems: 'center',
+ flexWrap: 'wrap'
+ }}
+ >
= ({
{session.startTime && session.startDelta !== undefined && (
-
+
{t('table.started-at', {
time: dayjs(session.startTime).format('HH:mm')
})}
@@ -88,9 +93,12 @@ export const SessionCard: React.FC = ({
)}
{session.status === 'in-progress' && session.startTime && (
-
+
{t('table.ends-at', {
time: dayjs(session.startTime).add(sessionLength, 'seconds').format('HH:mm')
})}
@@ -98,9 +106,12 @@ export const SessionCard: React.FC = ({
)}
>
) : (
-
+
{t('empty-state.no-team')}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-row.tsx
index c9a1210c4..97b54b5b8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/session-row.tsx
@@ -22,10 +22,11 @@ export const SessionRow: React.FC = ({ session, sessionLength }
+ }}
+ >
@@ -36,10 +37,11 @@ export const SessionRow: React.FC = ({ session, sessionLength }
direction="row"
spacing={0.5}
sx={{
- alignItems: "center",
- flexWrap: "wrap",
- justifyContent: "center"
- }}>
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ justifyContent: 'center'
+ }}
+ >
= ({ session, sessionLength }
{session.startTime && session.startDelta !== undefined && (
-
+
{t('table.started-at', {
time: dayjs(session.startTime).format('HH:mm')
})}
@@ -59,9 +64,12 @@ export const SessionRow: React.FC = ({ session, sessionLength }
)}
{session.status === 'in-progress' && session.startTime && (
-
+
{t('table.ends-at', {
time: dayjs(session.startTime).add(sessionLength, 'seconds').format('HH:mm')
})}
@@ -69,9 +77,12 @@ export const SessionRow: React.FC = ({ session, sessionLength }
)}
) : (
-
+
—
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/status-legend.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/status-legend.tsx
index 9d13456ef..3694611f0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/status-legend.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/judging-status/components/status-legend.tsx
@@ -108,9 +108,10 @@ export const StatusLegend: React.FC = ({ open, anchorEl, onCl
direction="row"
spacing={1.5}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
py: 0.5
- }}>
+ }}
+ >
+ width: '100%'
+ }}
+ >
} desktop={} />
setAnchorEl(null)} />
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/pit-map/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/pit-map/page.tsx
index 843d8e3cb..98bbddb90 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/pit-map/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/pit-map/page.tsx
@@ -31,18 +31,34 @@ export default function PitMapReportPage() {
alignItems: 'center'
}}
>
- {loading && {t('loading')}}
+ {loading && (
+
+ {t('loading')}
+
+ )}
- {error && !loading && {t('error-loading')}}
+ {error && !loading && (
+
+ {t('error-loading')}
+
+ )}
{!loading && !error && !pitMapUrl && (
- {t('no-map')}
+
+ {t('no-map')}
+
)}
{!loading && !error && pitMapUrl && (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/error-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/error-state.tsx
index 8c26f2a49..5adbd7091 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/error-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/error-state.tsx
@@ -8,9 +8,12 @@ export function ErrorState() {
return (
-
+
{t('error-loading')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/loading-state.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/loading-state.tsx
index 23dfe2894..6c2cb71aa 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/loading-state.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/loading-state.tsx
@@ -8,9 +8,13 @@ export function LoadingState() {
return (
- {t('loading')}
+
+ {t('loading')}
+
);
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/mobile-scoreboard.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/mobile-scoreboard.tsx
index f03d54c0f..bbe039694 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/mobile-scoreboard.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/mobile-scoreboard.tsx
@@ -19,14 +19,18 @@ export function MobileScoreboard({ data, matchesPerTeam }: MobileScoreboardProps
return (
-
+ }}
+ >
+
{t('no-data')}
@@ -107,9 +111,12 @@ export function MobileScoreboard({ data, matchesPerTeam }: MobileScoreboardProps
>
{entry.name}
-
+
#{entry.number}
@@ -118,17 +125,19 @@ export function MobileScoreboard({ data, matchesPerTeam }: MobileScoreboardProps
+ color: 'text.secondary',
+ fontSize: '0.75rem'
+ }}
+ >
{t('best-score')}
+ color: 'primary.main'
+ }}
+ >
{entry.maxScore ?? '-'}
@@ -141,17 +150,19 @@ export function MobileScoreboard({ data, matchesPerTeam }: MobileScoreboardProps
+ }}
+ >
{t('match-scores')}
+ }}
+ >
{Array.from({ length: matchesPerTeam }, (_, index) => {
const score = entry.scores[index];
return (
@@ -174,14 +185,18 @@ export function MobileScoreboard({ data, matchesPerTeam }: MobileScoreboardProps
+ display: 'block',
+ fontSize: '0.7rem'
+ }}
+ >
{t('match')} {index + 1}
-
+
{score ?? '-'}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/scoreboard-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/scoreboard-table.tsx
index 55c936601..e6d411088 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/scoreboard-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/scoreboard/components/scoreboard-table.tsx
@@ -61,11 +61,12 @@ export function ScoreboardTable({ data, matchesPerTeam }: ScoreboardTableProps)
noRowsOverlay: () => (
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%'
+ }}
+ >
{t('no-data')}
)
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/arrival-stats.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/arrival-stats.tsx
index 044d36ae4..2831914b0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/arrival-stats.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/arrival-stats.tsx
@@ -15,9 +15,13 @@ export const ArrivalStats: React.FC = ({ teams }) => {
const registeredCount = teams.filter(t => t.arrived).length;
return (
-
+
= ({ teams }) => {
{teams.length}
-
+
{t('total-teams')}
@@ -64,9 +71,12 @@ export const ArrivalStats: React.FC = ({ teams }) => {
>
{registeredCount}
-
+
{t('arrived-teams')}
@@ -96,9 +106,12 @@ export const ArrivalStats: React.FC = ({ teams }) => {
>
{teams.length - registeredCount}
-
+
{t('pending-teams')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/desktop-team-list-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/desktop-team-list-table.tsx
index 1d4f9c6a1..b2f5b6b35 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/desktop-team-list-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/desktop-team-list-table.tsx
@@ -80,9 +80,13 @@ export const DesktopTeamListTable: React.FC = ({ team
{sortedTeams.length === 0 ? (
- {t('no-teams')}
+
+ {t('no-teams')}
+
) : (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/mobile-team-list-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/mobile-team-list-table.tsx
index 8b930ebfa..6104362f2 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/mobile-team-list-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/components/mobile-team-list-table.tsx
@@ -50,14 +50,20 @@ export const MobileTeamListTable: React.FC = ({ teams
variant={team.arrived ? 'filled' : 'outlined'}
/>
-
+
{team.affiliation}
-
+
{team.city}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/page.tsx
index e6e589bfb..e1609c653 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/reports/team-list/page.tsx
@@ -51,9 +51,12 @@ export default function TeamListPage() {
{error && (
-
+
{t('error-loading')}
@@ -68,9 +71,13 @@ export default function TeamListPage() {
{loading && (
- {t('loading')}
+
+ {t('loading')}
+
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/audience-display-control.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/audience-display-control.tsx
index 23c460337..1dfce9443 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/audience-display-control.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/audience-display-control.tsx
@@ -52,9 +52,13 @@ export function AudienceDisplayControl() {
height: '100%'
}}
>
-
+
const showPreview = !isOnFinalSlide;
return (
-
+
{/* Display Area - Current and Next Slide */}
+ }}
+ >
{/* Current Slide */}
= ({ deckRef, t
direction="row"
spacing={1.5}
sx={{
- flexWrap: "wrap",
- justifyContent: "center"
- }}>
+ flexWrap: 'wrap',
+ justifyContent: 'center'
+ }}
+ >
= ({
flex: 1,
minWidth: 0,
maxWidth: '50%'
- }}>
+ }}
+ >
+ }}
+ >
{label}
{
+ }}
+ >
{
direction="row"
sx={{
gap: 0.75,
- alignItems: "center"
- }}>
+ alignItems: 'center'
+ }}
+ >
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/loaded-match/team-status-legend.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/loaded-match/team-status-legend.tsx
index 114b10608..8b9257007 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/loaded-match/team-status-legend.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/loaded-match/team-status-legend.tsx
@@ -96,9 +96,10 @@ export const TeamStatusLegend: React.FC = ({ open, anchor
direction="row"
spacing={1.5}
sx={{
- alignItems: "flex-start",
+ alignItems: 'flex-start',
py: 0.5
- }}>
+ }}
+ >
{
const iconProps = { sx: { fontSize: '1.5rem' } };
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/scorekeeper-loading-skeleton.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/scorekeeper-loading-skeleton.tsx
index d8f504a99..92b0d69d9 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/scorekeeper-loading-skeleton.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/components/scorekeeper-loading-skeleton.tsx
@@ -10,9 +10,12 @@ export function ScorekeeperLoadingSkeleton() {
{/* Audience Display Control */}
-
+
@@ -24,9 +27,12 @@ export function ScorekeeperLoadingSkeleton() {
{/* Control Buttons */}
-
+
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/subscriptions/match-completed.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/subscriptions/match-completed.ts
index 81139f295..6bba28111 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/subscriptions/match-completed.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/subscriptions/match-completed.ts
@@ -40,7 +40,9 @@ export function createMatchCompletedSubscription(divisionId: string) {
field: {
activeMatch: null,
matches: prev.division.field.matches.map(match =>
- match.id === completedMatchId ? { ...match, status: 'completed' as MatchStatus } : match
+ match.id === completedMatchId
+ ? { ...match, status: 'completed' as MatchStatus }
+ : match
)
}
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/types.ts
index b43ac8518..f6cc41562 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/types.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/graphql/types.ts
@@ -3,12 +3,7 @@ import { AwardsPresentation } from '@lems/database';
export type MatchStage = 'PRACTICE' | 'RANKING' | 'TEST';
export type MatchStatus = 'not-started' | 'in-progress' | 'completed';
export type AudienceDisplayScreen =
- | 'scoreboard'
- | 'match_preview'
- | 'sponsors'
- | 'logo'
- | 'message'
- | 'awards';
+ 'scoreboard' | 'match_preview' | 'sponsors' | 'logo' | 'message' | 'awards';
export interface TeamWinner {
team: {
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/page.tsx
index 487e5688e..0602578f1 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/scorekeeper/page.tsx
@@ -86,9 +86,12 @@ export default function ScorekeeperPage() {
-
+
{t('audience-display.section-title')}
@@ -97,9 +100,12 @@ export default function ScorekeeperPage() {
-
+
{t('controls.section-title')}
@@ -109,9 +115,12 @@ export default function ScorekeeperPage() {
{isAwardsMode ? (
-
+
{t('awards-presentation.title')}
@@ -121,9 +130,12 @@ export default function ScorekeeperPage() {
) : (
<>
-
+
{t('current-match.section-title')}
@@ -132,9 +144,12 @@ export default function ScorekeeperPage() {
-
+
{t('next-match.section-title')}
@@ -150,9 +165,10 @@ export default function ScorekeeperPage() {
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
{t('schedule.title')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/award-nominations.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/award-nominations.tsx
index f58133c92..8fdfcd6fd 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/award-nominations.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/award-nominations.tsx
@@ -74,9 +74,10 @@ export const AwardNominations: React.FC = ({ awards, disa
+ }}
+ >
{t('title')}
@@ -102,9 +103,12 @@ export const AwardNominations: React.FC = ({ awards, disa
/>
}
label={
-
+
{getName(awardName)}
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/feedback-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/feedback-row.tsx
index 29fce0926..540142bdb 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/feedback-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/feedback-row.tsx
@@ -85,9 +85,13 @@ export const FeedbackRow: React.FC = ({ category, disabled = f
}
}}
>
- {getFeedbackTitle(field)}...
+
+ {getFeedbackTitle(field)}...
+
))}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/field-rating-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/field-rating-row.tsx
index 4decd722a..a50e63b4c 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/field-rating-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/field-rating-row.tsx
@@ -65,7 +65,8 @@ export const FieldRatingRow: React.FC = ({
sx={{
alignItems: 'center',
justifyContent: label ? 'flex-start' : 'center'
- }}>
+ }}
+ >
= ({
{label && (
+ }}
+ >
{label}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/section-title-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/section-title-row.tsx
index 129e07219..364eaf967 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/section-title-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/section-title-row.tsx
@@ -31,16 +31,21 @@ export const SectionTitleRow: React.FC = ({ sectionId, cat
}}
colSpan={4}
>
-
+
+ }}
+ >
{getSectionTitle(sectionId)}
-
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/table-header-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/table-header-row.tsx
index bd3d435c1..e45fdc898 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/table-header-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/components/table-header-row.tsx
@@ -38,9 +38,10 @@ export const TableHeaderRow: React.FC = ({ category }) => {
>
+ }}
+ >
{getColumnTitle(column)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/page.tsx
index efccd81ee..a046a4da8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/page.tsx
@@ -102,9 +102,10 @@ export default function RubricPage() {
direction="row"
spacing={2}
sx={{
- justifyContent: "flex-end",
+ justifyContent: 'flex-end',
mt: 3
- }}>
+ }}
+ >
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/rubric-validation.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/rubric-validation.ts
index 686a97e39..454cb5b9f 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/rubric-validation.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/rubric/[category]/rubric-validation.ts
@@ -3,9 +3,7 @@ import { JudgingCategory } from '@lems/types/judging';
import { RubricItem } from './graphql';
export type ValidationError =
- | 'missing-field-value'
- | 'missing-notes-for-level-4'
- | 'missing-feedback';
+ 'missing-field-value' | 'missing-notes-for-level-4' | 'missing-feedback';
export interface ValidationResult {
isValid: boolean;
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/gp-selector.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/gp-selector.tsx
index 36d331854..2a931c2bf 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/gp-selector.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/gp-selector.tsx
@@ -109,11 +109,12 @@ export const GPSelector: React.FC = ({ disabled = false }) => {
+ textAlign: 'center'
+ }}
+ >
{t('title')}
= ({ disabled = false }) => {
>
+ }}
+ >
{t(`level-${column.value}`)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/mission-clause.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/mission-clause.tsx
index 154958223..29a7e85ca 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/mission-clause.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/mission-clause.tsx
@@ -130,14 +130,20 @@ export const MissionClause: React.FC = ({
return (
-
+
{description}
-
+
{clause.type === 'boolean' ? (
-
+
{t('participant-not-present-title')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/score-floater.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/score-floater.tsx
index 633c760f1..1a385d54e 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/score-floater.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/score-floater.tsx
@@ -35,12 +35,18 @@ export const ScoreFloater: React.FC = ({ score }) => {
}}
elevation={0}
>
-
-
+
+
= (
const submitButtonHandler = shouldSwitchToGP ? onSwitchToGP : onSubmit;
return (
-
+
{showEscalateButton && (
= (
+ }}
+ >
{t('processing')}
@@ -93,9 +98,10 @@ export const ScoresheetActionButtons: React.FC = (
+ }}
+ >
{t('processing')}
@@ -126,9 +132,10 @@ export const ScoresheetActionButtons: React.FC = (
+ }}
+ >
{t('submitting')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-alert.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-alert.tsx
index 7a7d42377..eda1ee8ef 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-alert.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-alert.tsx
@@ -53,10 +53,11 @@ export const ScoresheetIncompleteAlert: React.FC
+ }}
+ >
{t('incomplete-action')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-form.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-form.tsx
index 0d41f63df..0e92a5495 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-form.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-form.tsx
@@ -23,7 +23,8 @@ export const ScoresheetForm: React.FC = ({ disabled = false
sx={{
mt: 4,
mb: 16
- }}>
+ }}
+ >
{scoresheet.missions.map((mission, index) => (
= ({ disabled = false
{validation.validatorErrors.map(errorId => (
- {getError(errorId)}
+
+ {getError(errorId)}
+
))}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-mission.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-mission.tsx
index b41b9b2b5..ebf3b3ee8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-mission.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/components/scoresheet-mission.tsx
@@ -73,40 +73,55 @@ const ScoresheetMission: React.FC = ({
};
return (
-
+
+ borderRadius: '6px 0 0 0',
+ textAlign: 'center'
+ }}
+ >
+ }}
+ >
{mission.id.toUpperCase()}
-
-
+
+
+ }}
+ >
{title}
{mission.noEquipment && (
@@ -123,11 +138,12 @@ const ScoresheetMission: React.FC = ({
+ }}
+ >
{description}
@@ -149,18 +165,22 @@ const ScoresheetMission: React.FC = ({
/>
);
})}
-
+
{remarks.map((remark, index) => (
+ }}
+ >
{remark}
))}
@@ -172,13 +192,18 @@ const ScoresheetMission: React.FC = ({
sx={{
mt: 2,
ml: 3
- }}>
+ }}
+ >
{missionErrors.map(errorId => (
- {getError(errorId)}
+
+ {getError(errorId)}
+
))}
@@ -191,7 +216,8 @@ const ScoresheetMission: React.FC = ({
borderRadius: 8,
p: 2,
display: { xs: 'none', sm: 'block' }
- }}>
+ }}
+ >
= ({ isSigned, on
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "space-between",
+ alignItems: 'center',
+ justifyContent: 'space-between',
mt: 2
- }}>
+ }}
+ >
{isSigned && (
{
const svgElement = signaturePadRef.current?.svg;
if (!svgElement) return '';
-
+
const svgString = new XMLSerializer().serializeToString(svgElement);
return `data:image/svg+xml;base64,${btoa(svgString)}`;
}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/page.tsx
index 67164da61..b682aac1b 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/team/[teamSlug]/scoresheet/[scoresheetSlug]/page.tsx
@@ -96,9 +96,10 @@ export default function ScoresheetPage() {
direction="row"
spacing={2}
sx={{
- justifyContent: "flex-end",
- alignItems: "top"
- }}>
+ justifyContent: 'flex-end',
+ alignItems: 'top'
+ }}
+ >
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/missing-teams-alert.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/missing-teams-alert.tsx
index aacae75af..a4144658f 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/missing-teams-alert.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/missing-teams-alert.tsx
@@ -27,9 +27,14 @@ export function MissingTeamsAlert({
? `${t('slots.missing-teams.from-round')}: ${currentRoundTitle}`
: t('slots.missing-teams.title')}
-
+
{missingTeams.map(team => (
+ }}
+ >
{t('match-schedule.columns.match-number')}
@@ -179,7 +180,8 @@ function FieldScheduleTableComponent({ isMobile }: FieldScheduleTableProps) {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('match-schedule.columns.start-time')}
@@ -188,7 +190,8 @@ function FieldScheduleTableComponent({ isMobile }: FieldScheduleTableProps) {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('match-schedule.columns.end-time')}
@@ -197,7 +200,8 @@ function FieldScheduleTableComponent({ isMobile }: FieldScheduleTableProps) {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('match-schedule.columns.status')}
@@ -207,7 +211,8 @@ function FieldScheduleTableComponent({ isMobile }: FieldScheduleTableProps) {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{table.name}
@@ -225,30 +230,33 @@ function FieldScheduleTableComponent({ isMobile }: FieldScheduleTableProps) {
+ }}
+ >
{match.number}
+ }}
+ >
{matchTime.format('HH:mm')}
+ }}
+ >
{endTime.format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/schedule/judging-schedule-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/schedule/judging-schedule-table.tsx
index 2b8cb389a..7ffb4b970 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/schedule/judging-schedule-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/schedule/judging-schedule-table.tsx
@@ -75,7 +75,8 @@ function JudgingScheduleTableComponent({ isMobile }: JudgingScheduleTableProps)
sx={{
fontWeight: 600,
fontSize: headerFontSize
- }}>
+ }}
+ >
{t('judging-schedule.columns.start-time')}
@@ -84,7 +85,8 @@ function JudgingScheduleTableComponent({ isMobile }: JudgingScheduleTableProps)
sx={{
fontWeight: 600,
fontSize: headerFontSize
- }}>
+ }}
+ >
{t('judging-schedule.columns.end-time')}
@@ -94,7 +96,8 @@ function JudgingScheduleTableComponent({ isMobile }: JudgingScheduleTableProps)
sx={{
fontWeight: 600,
fontSize: headerFontSize
- }}>
+ }}
+ >
{room.name}
@@ -112,20 +115,22 @@ function JudgingScheduleTableComponent({ isMobile }: JudgingScheduleTableProps)
+ }}
+ >
{sessionTime.format('HH:mm')}
+ }}
+ >
{sessionEndTime.format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/field-matches-list.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/field-matches-list.tsx
index a4a360f76..9fa058f60 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/field-matches-list.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/field-matches-list.tsx
@@ -21,9 +21,13 @@ export function FieldMatchesList({
return (
-
+
{t('field-matches')}
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+ }}
+ >
{getStage?.(match.stage)} #{match.number} • {participant?.table.name} •{' '}
{dayjs(match.scheduledTime).format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/judging-sessions-list.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/judging-sessions-list.tsx
index dfe5a640f..23201d469 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/judging-sessions-list.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/judging-sessions-list.tsx
@@ -19,9 +19,13 @@ export function JudgingSessionsList({
return (
-
+
{t('judging-sessions')}
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+ }}
+ >
{t('labels.session')} #{session.number} • {session.room.name} •{' '}
{dayjs(session.scheduledTime).format('HH:mm')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/second-slot-info.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/second-slot-info.tsx
index ed221a602..1c4cacc68 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/second-slot-info.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/second-slot-info.tsx
@@ -30,31 +30,46 @@ export function SecondSlotInfo({
<>
-
+
{t('slots.second-team-selected')}
{secondSlot.team ? (
<>
-
+
#{secondSlot.team.number} {secondSlot.team.name}
{(secondSlot?.team?.affiliation || secondSlot?.team?.city) && (
-
+
{[secondSlot.team.affiliation, secondSlot.team.city].filter(Boolean).join(' • ')}
)}
>
) : (
-
+
{t('labels.no-team')}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/selected-slot-header.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/selected-slot-header.tsx
index e3d6ba993..ac2eb54d5 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/selected-slot-header.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/selected-slot-header.tsx
@@ -19,9 +19,10 @@ export function SelectedSlotHeader({ selectedSlot, onClose }: SelectedSlotHeader
+ color: 'text.secondary',
+ display: 'block'
+ }}
+ >
{selectedSlot?.type === 'match'
? `${t('labels.match')} - ${selectedSlot.tableName}`
: `${t('labels.session')} - ${selectedSlot?.roomName}`}
@@ -31,21 +32,31 @@ export function SelectedSlotHeader({ selectedSlot, onClose }: SelectedSlotHeader
+ color: 'warning.main',
+ display: 'block'
+ }}
+ >
{t('labels.not-assigned')}
)}
-
+
#{selectedSlot?.team?.number} {selectedSlot?.team?.name}
{(selectedSlot?.team?.affiliation || selectedSlot?.team?.city) && (
-
+
{[selectedSlot.team.affiliation, selectedSlot.team.city].filter(Boolean).join(' • ')}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/team-selection-drawer.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/team-selection-drawer.tsx
index d59413964..e1716bf48 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/team-selection-drawer.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-selection-drawer/team-selection-drawer.tsx
@@ -99,9 +99,10 @@ export function TeamSelectionDrawer({
+ color: 'text.secondary',
+ textAlign: 'center'
+ }}
+ >
{t('slots.select-second-team-instruction')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-slot.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-slot.tsx
index 4a332130d..270918121 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-slot.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/(dashboard)/tournament-manager/components/team-slot.tsx
@@ -164,9 +164,12 @@ function TeamSlotComponent({
)}
) : (
-
+
-
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/match-preview/match-participant-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/match-preview/match-participant-card.tsx
index 2506b989c..9f514dfad 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/match-preview/match-participant-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/match-preview/match-participant-card.tsx
@@ -14,20 +14,25 @@ export const MatchParticipantCard: React.FC = ({ part
}
return (
-
+
alpha(theme.palette.primary.main, 0.05)
- }}>
+ }}
+ >
{
animation: 'fadeInScale 0.6s ease-out'
}}
>
-
-
+
+
+ display: 'flex',
+ justifyContent: 'center'
+ }}
+ >
{
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center'
+ }}
+ >
{
+ display: 'flex',
+ justifyContent: 'center'
+ }}
+ >
{
);
return (
-
+
+ display: 'flex',
+ justifyContent: 'center'
+ }}
+ >
{
sx={{
p: 1.5,
borderRadius: 2,
- alignItems: "flex-start",
+ alignItems: 'flex-start',
bgcolor: theme => alpha(theme.palette.background.paper, 0.98)
- }}>
-
+ }}
+ >
+
{
+ alignItems: 'flex-end',
+ width: '100%'
+ }}
+ >
{
+ display: 'flex',
+ justifyContent: 'center'
+ }}
+ >
{
+ }}
+ >
{(scoreboardSettings?.showActiveMatch as boolean) && }
{(scoreboardSettings?.showPreviousMatch as boolean) && }
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/scoreboard/team-score-card.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/scoreboard/team-score-card.tsx
index 8b6caf556..a1e4adf31 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/scoreboard/team-score-card.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/components/scoreboard/team-score-card.tsx
@@ -48,9 +48,10 @@ export const TeamScoreCard: React.FC = ({
+ alignItems: 'flex-start',
+ justifyContent: 'center'
+ }}
+ >
= ({ open, onClose }) =
sx={{
pt: 3,
pb: 2
- }}>
+ }}
+ >
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/graphql/types.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/graphql/types.ts
index 7ff171a4f..dcff30bb1 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/graphql/types.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/graphql/types.ts
@@ -1,12 +1,7 @@
import { AwardsPresentation } from '@lems/database';
export type AudienceDisplayScreen =
- | 'scoreboard'
- | 'match_preview'
- | 'sponsors'
- | 'logo'
- | 'message'
- | 'awards';
+ 'scoreboard' | 'match_preview' | 'sponsors' | 'logo' | 'message' | 'awards';
export interface AudienceDisplayState {
activeDisplay: AudienceDisplayScreen;
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/page.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/page.tsx
index f8c8102da..1a5af2288 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/audience-display/page.tsx
@@ -127,9 +127,7 @@ export default function AudienceDisplayPage() {
awards={data.awards}
awardWinnerSlideStyle={
(data.displayState.settings?.awards?.awardWinnerSlideStyle as
- | 'chroma'
- | 'full'
- | 'both') || 'both'
+ 'chroma' | 'full' | 'both') || 'both'
}
/>
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/components/connection-indicator.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/components/connection-indicator.tsx
index 81b04d11e..634e498f5 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/components/connection-indicator.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/components/connection-indicator.tsx
@@ -114,9 +114,10 @@ export function ConnectionIndicator({ compact = false }: ConnectionIndicatorProp
component="span"
sx={{
flex: 1,
- textAlign: "center",
+ textAlign: 'center',
userSelect: 'none'
- }}>
+ }}
+ >
{t(state)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/complete-deliberation-modal.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/complete-deliberation-modal.tsx
index 18e076903..bc95a717c 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/complete-deliberation-modal.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/complete-deliberation-modal.tsx
@@ -71,9 +71,12 @@ export const CompleteDeliberationModal: React.FC
{t('title')}
-
+
{t('description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx
index 6cf875731..b347ada9f 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/deliberation-table.tsx
@@ -121,7 +121,7 @@ export function DeliberationTable() {
// Rubric field columns
...(fieldDisplayLabels || []).map(
label =>
- (({
+ ({
field: label,
headerName: label,
width: FIELD_COLUMN_WIDTH,
@@ -134,7 +134,7 @@ export function DeliberationTable() {
const value = params.row.rubricFields[label];
return value !== null ? value : '-';
}
- }) as GridColDef)
+ }) as GridColDef
),
// GP score columns (only shown for core-values category)
@@ -147,7 +147,7 @@ export function DeliberationTable() {
})
.map(
gpKey =>
- (({
+ ({
field: gpKey,
headerName: gpKey,
width: FIELD_COLUMN_WIDTH,
@@ -160,7 +160,7 @@ export function DeliberationTable() {
const value = params.row.gpScores[gpKey];
return value ?? 3; // Default GP score is 3 if not set
}
- }) as GridColDef)
+ }) as GridColDef
)
: []),
{
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/metrics.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/metrics.tsx
index 1f757d0dc..bd5f584a7 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/metrics.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/components/metrics.tsx
@@ -15,9 +15,13 @@ export function Metrics() {
: undefined;
return (
-
+
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts
index f99ec6a8e..4ad65c972 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/[category]/deliberation-computation.ts
@@ -23,10 +23,7 @@ export function computeRank(
category: JudgingCategory | 'total' = 'total'
): number {
const categoryKey = underscoresToHyphens(category) as
- | 'innovation-project'
- | 'robot-design'
- | 'core-values'
- | 'total';
+ 'innovation-project' | 'robot-design' | 'core-values' | 'total';
// Helper: compute rank by category
const computeRankByCategory = (category: keyof MetricPerCategory, teamScore: number): number => {
// Count how many teams have a higher score
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/category-filter.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/category-filter.tsx
index 7f178150c..2ecbd842e 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/category-filter.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/category-filter.tsx
@@ -38,9 +38,10 @@ export function CategoryFilter({ currentCategory, compact = false }: CategoryFil
spacing={1}
useFlexGap
sx={{
- flexWrap: "wrap",
- alignItems: "center"
- }}>
+ flexWrap: 'wrap',
+ alignItems: 'center'
+ }}
+ >
{tCompare('filter-by-category')}
-
+
-
+
{t('no-teams-selected')}
+ }}
+ >
{t('no-teams-description')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/exceeding-notes.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/exceeding-notes.tsx
index dbf67a8a7..cc4b7011a 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/exceeding-notes.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/exceeding-notes.tsx
@@ -33,9 +33,13 @@ function NoteItem({
}) {
return (
-
+
@@ -80,9 +84,12 @@ export function ExceedingNotes({ team }: ExceedingNotesProps) {
return (
-
+
{t('exceeding-notes')}
{category ? (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/feedback.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/feedback.tsx
index 8ce21f94e..68c0f357e 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/feedback.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/feedback.tsx
@@ -156,9 +156,12 @@ export function Feedback({ team }: FeedbackProps) {
return (
-
+
{t('feedback')}
{category ? (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/gp-scores.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/gp-scores.tsx
index 024b9892a..a9f0601f0 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/gp-scores.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/gp-scores.tsx
@@ -19,9 +19,12 @@ export function GpScores({ team }: GpScoresProps) {
return (
-
+
{t('gp-scores')}
@@ -44,10 +47,11 @@ export function GpScores({ team }: GpScoresProps) {
+ }}
+ >
{t('round')} {scoresheet.round}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/nominations.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/nominations.tsx
index b348580e6..50ad457e5 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/nominations.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/nominations.tsx
@@ -43,9 +43,12 @@ export function Nominations({ team }: NominationsProps) {
return (
-
+
{t('nominations')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/radar-charts.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/radar-charts.tsx
index 5bd780ba1..8a88cafe8 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/radar-charts.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/radar-charts.tsx
@@ -61,9 +61,10 @@ export const CategoryRadarChart = ({ team, category }: CategoryRadarChartProps)
+ color: 'text.secondary',
+ textAlign: 'center'
+ }}
+ >
{t('no-rubric-data')}
);
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/rubric-scores.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/rubric-scores.tsx
index c0caf74ba..e06f49efc 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/rubric-scores.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/rubric-scores.tsx
@@ -159,9 +159,10 @@ export const RubricScores = ({ team }: RubricScoresProps) => {
+ color: 'text.secondary',
+ textAlign: 'center'
+ }}
+ >
{t('no-rubric-data')}
);
@@ -174,7 +175,8 @@ export const RubricScores = ({ team }: RubricScoresProps) => {
sx={{
fontWeight: 600,
fontSize: '1.1rem'
- }}>
+ }}
+ >
{t('rubric-scores')}
{category ? (
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/score-summary.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/score-summary.tsx
index 346e4b0cc..2c37bd148 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/score-summary.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/score-summary.tsx
@@ -29,60 +29,83 @@ export function ScoreSummary({ team }: ScoreSummaryProps) {
sx={{
fontWeight: 600,
fontSize: '1.1rem'
- }}>
+ }}
+ >
{t('field-comparison')}
-
-
+
+
+ }}
+ >
{comparison.wins}
-
+
{t('wins')}
-
+
+ }}
+ >
{comparison.ties}
-
+
{t('ties')}
-
+
+ }}
+ >
{comparison.losses}
-
+
{t('losses')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/section-score-row.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/section-score-row.tsx
index b6c8bccfb..33dccf5a4 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/section-score-row.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/section-score-row.tsx
@@ -24,9 +24,10 @@ export function SectionScoreRow({
direction="row"
spacing={1}
sx={{
- justifyContent: "space-between",
- alignItems: "center"
- }}>
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
{showSectionName && (
-
+
{team.name} - #{team.number}
{team.profileDocumentUrl && (
@@ -51,15 +54,21 @@ export function TeamInfo({ team }: TeamInfoProps) {
)}
-
+
{team.affiliation}
{team.judgingSession?.room && (
-
+
{t('judging-room')}: {team.judgingSession.room.name}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/team-selector.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/team-selector.tsx
index 6964eddc6..55d26719d 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/team-selector.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/compare/components/team-selector.tsx
@@ -118,14 +118,20 @@ export const TeamSelector = ({ currentTeams, compact = false }: TeamSelectorProp
+ }}
+ >
{t('select-teams-to-compare')} ({currentTeams.length}/6)
-
+
{currentTeamObjects.length > 0 ? (
currentTeamObjects.map(team => (
))
) : (
-
+
{t('no-teams-selected')}
)}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/room-metrics.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/room-metrics.tsx
index 1acc41d73..98f70056a 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/room-metrics.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/room-metrics.tsx
@@ -95,9 +95,12 @@ export function RoomScoresDistribution({ roomMetrics, teams }: RoomScoresDistrib
justifyContent: 'center'
}}
>
-
+
{t('no-data-available')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/small-screen-block.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/small-screen-block.tsx
index b407ff17e..2b853722d 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/small-screen-block.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/components/small-screen-block.tsx
@@ -33,10 +33,11 @@ export const SmallScreenBlock = () => {
+ }}
+ >
{t('minimum-width')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/champions/champions-podium.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/champions/champions-podium.tsx
index f511ab21d..037158572 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/champions/champions-podium.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/champions/champions-podium.tsx
@@ -139,9 +139,12 @@ export function ChampionsPodium() {
})}
{/* Team Selection Dropdowns */}
-
+
{places.map(place => (
{t('place-label', { place })}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx
index 01fa91a74..0538749d2 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/optional-awards/optional-awards-data-grid.tsx
@@ -163,7 +163,7 @@ export function OptionalAwardsDataGrid() {
},
...Object.keys(teams[0]?.rubricsFields?.['core-values']).map(
label =>
- (({
+ ({
field: label,
headerName: label,
width: FIELD_COLUMN_WIDTH,
@@ -175,7 +175,7 @@ export function OptionalAwardsDataGrid() {
const value = params.row.rubricsFields['core-values'][label];
return value !== null ? value : '-';
}
- }) as GridColDef)
+ }) as GridColDef
),
// GP score columns (only shown for core-values category)
@@ -187,7 +187,7 @@ export function OptionalAwardsDataGrid() {
})
.map(
gpKey =>
- (({
+ ({
field: gpKey,
headerName: gpKey,
width: FIELD_COLUMN_WIDTH,
@@ -199,7 +199,7 @@ export function OptionalAwardsDataGrid() {
const value = params.row.gpScores[gpKey];
return value !== null ? value : '-';
}
- }) as GridColDef)
+ }) as GridColDef
),
{
field: 'nominations',
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/review/review-stage.tsx b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/review/review-stage.tsx
index 81993206c..cde74998b 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/review/review-stage.tsx
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/components/review/review-stage.tsx
@@ -82,10 +82,11 @@ export const ReviewStage: React.FC = () => {
+ }}
+ >
{t('title')}
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/graphql/subscriptions/final-deliberation-updated.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/graphql/subscriptions/final-deliberation-updated.ts
index 9adb5009b..a656a1477 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/graphql/subscriptions/final-deliberation-updated.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/final/graphql/subscriptions/final-deliberation-updated.ts
@@ -59,10 +59,7 @@ const finalDeliberationUpdatedReconciler: Reconciler)
} as Record;
const camelCaseKey = kebabCaseToCamelCase(key) as
- | 'robot-performance'
- | 'innovation-project'
- | 'robot-design'
- | 'core-values';
+ 'robot-performance' | 'innovation-project' | 'robot-design' | 'core-values';
awardsUpdate[camelCaseKey] = value as string[];
}
} catch {
diff --git a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/utils.ts b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/utils.ts
index 2078975c5..b9a65954e 100644
--- a/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/utils.ts
+++ b/apps/frontend/src/app/[locale]/lems/(volunteer)/deliberation/utils.ts
@@ -176,9 +176,7 @@ export function getOrganizedRubricFields(
const result: Record = {};
const categoryKey = hyphensToUnderscores(category) as
- | 'innovation_project'
- | 'robot_design'
- | 'core_values';
+ 'innovation_project' | 'robot_design' | 'core_values';
const categoryMap = {
innovation_project: team.rubrics.innovation_project,
diff --git a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/combined-feedback-table.tsx b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/combined-feedback-table.tsx
index e4307f0d8..abec166d8 100644
--- a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/combined-feedback-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/combined-feedback-table.tsx
@@ -89,7 +89,8 @@ export const CombinedFeedbackTable: React.FC = ({ ru
lineHeight: '12px',
height: '12px'
}
- }}>
+ }}
+ >
{getFeedbackTitle('thinkAbout')}
@@ -120,7 +121,8 @@ export const CombinedFeedbackTable: React.FC = ({ ru
lineHeight: '12px',
height: '12px'
}
- }}>
+ }}
+ >
{getFeedbackTitle('greatJob')}
diff --git a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/export-rubric-table.tsx b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/export-rubric-table.tsx
index 641487999..7aba7f6cc 100644
--- a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/export-rubric-table.tsx
+++ b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/components/export-rubric-table.tsx
@@ -112,9 +112,10 @@ export const ExportRubricTable: React.FC = ({
>
+ }}
+ >
{getColumnTitle(column)}
@@ -137,16 +138,21 @@ export const ExportRubricTable: React.FC = ({
fontWeight: 500
}}
>
-
+
+ }}
+ >
{getSectionTitle(section.id)}
-
@@ -191,7 +197,8 @@ export const ExportRubricTable: React.FC = ({
sx={{
alignItems: levelText ? 'flex-start' : 'center',
justifyContent: levelText ? 'flex-start' : 'center'
- }}>
+ }}
+ >
{field.coreValues ? (
isChecked ? (
= ({
/>
)}
{levelText && (
- {levelText}
+
+ {levelText}
+
)}
diff --git a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/page.tsx b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/page.tsx
index 8143aa571..5eb1448fd 100644
--- a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/rubrics/page.tsx
@@ -244,9 +244,10 @@ export default function RubricsExportPage() {
+ }}
+ >
{t('feedback.awards.title')}
@@ -272,7 +273,8 @@ export default function RubricsExportPage() {
sx={{
fontWeight: 500,
fontSize: '0.875rem'
- }}>
+ }}
+ >
{getAwardName(awardName)}
}
diff --git a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/components/export-scoresheet-mission.tsx b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/components/export-scoresheet-mission.tsx
index 30e68b307..17ecfcf8f 100644
--- a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/components/export-scoresheet-mission.tsx
+++ b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/components/export-scoresheet-mission.tsx
@@ -84,8 +84,9 @@ export const ExportScoresheetMission: React.FC = (
+ fontSize: '0.9rem'
+ }}
+ >
{mission.id.toUpperCase()}
@@ -93,9 +94,10 @@ export const ExportScoresheetMission: React.FC = (
+ }}
+ >
{title}
@@ -105,9 +107,10 @@ export const ExportScoresheetMission: React.FC = (
+ }}
+ >
{description}
@@ -121,16 +124,20 @@ export const ExportScoresheetMission: React.FC = (
+ }}
+ >
{getClauseDescription(clauseIndex)}
{valueDisplay && (
<>
-
+
{' '}
= (
>
+ }}
+ >
{score}
diff --git a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/page.tsx b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/page.tsx
index b508f03ff..6f012b1fe 100644
--- a/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/page.tsx
+++ b/apps/frontend/src/app/[locale]/lems/export/[teamSlug]/[eventSlug]/scoresheets/page.tsx
@@ -82,7 +82,7 @@ export default function ScoresExportPage() {
{scoresheet.missions
- .map((mission) => {
+ .map(mission => {
const missionData = scoresheetData.missions?.find(m => m.id === mission.id);
if (!missionData || !missionData.clauses || missionData.clauses.length === 0) {
return null;
@@ -120,9 +120,10 @@ export default function ScoresExportPage() {
>
+ }}
+ >
{t('total-points', { points: scoresheetData.score ?? 0 })}
diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json
index 536e39367..193e062f6 100644
--- a/apps/frontend/tsconfig.json
+++ b/apps/frontend/tsconfig.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"jsx": "preserve",
"strict": true,
+ "types": ["node", "@types/webpack-env", "@types/grecaptcha"],
"noEmit": true,
"emitDeclarationOnly": false,
"esModuleInterop": true,
@@ -12,7 +13,6 @@
"isolatedModules": true,
"lib": [
"dom",
- "dom.iterable",
"esnext"
],
"allowJs": true,
diff --git a/apps/portal/locale/he.json b/apps/portal/locale/he.json
index 6590bf2b7..b8782d717 100644
--- a/apps/portal/locale/he.json
+++ b/apps/portal/locale/he.json
@@ -284,4 +284,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/portal/src/app/[locale]/components/app-bar.tsx b/apps/portal/src/app/[locale]/components/app-bar.tsx
index 41193f300..e602bee59 100644
--- a/apps/portal/src/app/[locale]/components/app-bar.tsx
+++ b/apps/portal/src/app/[locale]/components/app-bar.tsx
@@ -62,11 +62,12 @@ const DesktopAppBar: React.FC = () => {
sx={{
display: { xs: 'none', md: 'flex' },
mr: 1,
- height: "44px",
- width: "164px",
- position: "relative",
+ height: '44px',
+ width: '164px',
+ position: 'relative',
cursor: 'pointer'
- }}>
+ }}
+ >
{
+ justifyContent: 'center',
+ alignItems: 'center'
+ }}
+ >
+ }}
+ >
= ({
spacing={2}
onClick={() => setExpanded(!expanded)}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mt: variant === 'active' ? 0 : 4,
mb: 2,
cursor: 'pointer'
- }}>
+ }}
+ >
{showIcon}
+ }}
+ >
{title}
@@ -78,10 +80,11 @@ export const EventsSection: React.FC = ({
{events.length === 0 ? (
+ color: 'text.secondary'
+ }}
+ >
{emptyMessage || t('no-events')}
@@ -99,10 +102,11 @@ export const EventsSection: React.FC = ({
+ }}
+ >
{t('more-events', { count: events.length - maxDisplayed!, variant })}
)}
diff --git a/apps/portal/src/app/[locale]/components/homepage/hero.tsx b/apps/portal/src/app/[locale]/components/homepage/hero.tsx
index 227146966..9d262f54e 100644
--- a/apps/portal/src/app/[locale]/components/homepage/hero.tsx
+++ b/apps/portal/src/app/[locale]/components/homepage/hero.tsx
@@ -20,22 +20,24 @@ const HeroContainer = ({ children }: { children: React.ReactNode }) => {
return (
+ }}
+ >
+ }}
+ />
{children}
);
@@ -60,13 +62,15 @@ export const Hero: React.FC = ({ season }) => {
sx={{
alignItems: { xs: 'center', sm: 'flex-start' },
textAlign: { xs: 'center', sm: 'left' }
- }}>
+ }}
+ >
+ }}
+ >
{t('title')}
= ({ season }) => {
maxWidth: 600,
fontSize: { xs: '1.1rem', sm: '1.25rem', md: '1.5rem' },
opacity: 0.9
- }}>
+ }}
+ >
{tags => t.rich('subtitle', tags)}
diff --git a/apps/portal/src/app/[locale]/components/homepage/live-icon.tsx b/apps/portal/src/app/[locale]/components/homepage/live-icon.tsx
index 8e4cca501..9c01e0a0d 100644
--- a/apps/portal/src/app/[locale]/components/homepage/live-icon.tsx
+++ b/apps/portal/src/app/[locale]/components/homepage/live-icon.tsx
@@ -13,13 +13,13 @@ export const LiveIcon: React.FC = () => {
{
animation: `${liveAnimation} 2s ease-in-out infinite`,
zIndex: -1
}
- }} />
+ }}
+ />
);
};
diff --git a/apps/portal/src/app/[locale]/components/homepage/quick-actions-section.tsx b/apps/portal/src/app/[locale]/components/homepage/quick-actions-section.tsx
index b82b2eaaf..4b99844f8 100644
--- a/apps/portal/src/app/[locale]/components/homepage/quick-actions-section.tsx
+++ b/apps/portal/src/app/[locale]/components/homepage/quick-actions-section.tsx
@@ -49,13 +49,17 @@ export const QuickActionsSection = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
-
+
{t('title')}
@@ -77,9 +81,13 @@ export const QuickActionsSection = () => {
onClick={() => (window.location.href = action.href)}
>
-
+
{
-
+
{t(action.title)}
-
+
{t(action.description)}
diff --git a/apps/portal/src/app/[locale]/components/homepage/resource-links-section.tsx b/apps/portal/src/app/[locale]/components/homepage/resource-links-section.tsx
index d6fa2d8fb..6029040aa 100644
--- a/apps/portal/src/app/[locale]/components/homepage/resource-links-section.tsx
+++ b/apps/portal/src/app/[locale]/components/homepage/resource-links-section.tsx
@@ -51,13 +51,17 @@ export const ResourceLinksSection = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 3
- }}>
+ }}
+ >
-
+
{t('title')}
@@ -85,9 +89,13 @@ export const ResourceLinksSection = () => {
}}
>
-
+
{
-
+
{t(resource.title)}
-
+
{t(resource.description)}
diff --git a/apps/portal/src/app/[locale]/components/homepage/search/search-section.tsx b/apps/portal/src/app/[locale]/components/homepage/search/search-section.tsx
index 15473dea5..c78cb4213 100644
--- a/apps/portal/src/app/[locale]/components/homepage/search/search-section.tsx
+++ b/apps/portal/src/app/[locale]/components/homepage/search/search-section.tsx
@@ -56,9 +56,10 @@ export const SearchSection = () => {
+ }}
+ >
{t('title')}
@@ -99,7 +100,8 @@ export const SearchSection = () => {
'& .MuiOutlinedInput-root': {
paddingRight: 1
}
- }} />
+ }}
+ />
{
>
{isSearching && (
@@ -136,9 +141,12 @@ export const SearchSection = () => {
{noResults && (
@@ -147,12 +155,19 @@ export const SearchSection = () => {
{!noResults && (
<>
-
+
{Object.values(groupedMatches).map((roundObject, roundIndex) => (
@@ -103,9 +106,11 @@ export const FieldScheduleTab: React.FC = () => {
border: 'none'
}}
>
-
+
{t('field-schedule.round', {
stage: getStage(roundObject.stage),
number: roundObject.round
@@ -119,7 +124,8 @@ export const FieldScheduleTab: React.FC = () => {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('field-schedule.match')}
@@ -128,17 +134,19 @@ export const FieldScheduleTab: React.FC = () => {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('field-schedule.time')}
- {tables.map((table) => (
+ {tables.map(table => (
+ }}
+ >
{table.name}
@@ -153,17 +161,19 @@ export const FieldScheduleTab: React.FC = () => {
sx={{
fontWeight: 500,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{match.number}
+ }}
+ >
{dayjs(match.scheduledTime).format('HH:mm')}
@@ -194,9 +204,10 @@ export const FieldScheduleTab: React.FC = () => {
) : (
+ }}
+ >
-
)}
@@ -214,15 +225,19 @@ export const FieldScheduleTab: React.FC = () => {
{Object.keys(groupedMatches).length === 0 && (
-
+ }}
+ >
+
{t('field-schedule.no-data')}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/judging-schedule-tab.tsx b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/judging-schedule-tab.tsx
index 356d2609d..ca3512f18 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/judging-schedule-tab.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/judging-schedule-tab.tsx
@@ -78,7 +78,8 @@ export const JudgingScheduleTab = () => {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{t('judging-schedule.start-time')}
@@ -88,7 +89,8 @@ export const JudgingScheduleTab = () => {
sx={{
fontWeight: 600,
fontSize: isMobile ? '0.75rem' : '1rem'
- }}>
+ }}
+ >
{room.name}
@@ -101,10 +103,11 @@ export const JudgingScheduleTab = () => {
+ }}
+ >
{dayjs(session.time).format('HH:mm')}
@@ -133,9 +136,10 @@ export const JudgingScheduleTab = () => {
) : (
+ }}
+ >
-
)}
@@ -152,15 +156,19 @@ export const JudgingScheduleTab = () => {
{groupedSessions.length === 0 && (
-
+ }}
+ >
+
{t('judging-schedule.no-data')}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/loading-tab.tsx b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/loading-tab.tsx
index 938924222..f8998d3de 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/loading-tab.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/loading-tab.tsx
@@ -11,9 +11,12 @@ export const LoadingTab = () => {
-
+
{/* Header skeleton */}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/desktop-scoreboard.tsx b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/desktop-scoreboard.tsx
index 0249eeed0..539197f7f 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/desktop-scoreboard.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/desktop-scoreboard.tsx
@@ -84,11 +84,12 @@ export const DesktopScoreboard: React.FC = ({
noRowsOverlay: () => (
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: '100%'
+ }}
+ >
{t('scoreboard.no-data')}
)
diff --git a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/mobile-scoreboard.tsx b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/mobile-scoreboard.tsx
index 14fe1ac2f..2899f8917 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/mobile-scoreboard.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/scoreboard/mobile-scoreboard.tsx
@@ -26,14 +26,18 @@ export const MobileScoreboard: React.FC = ({
return (
-
+ }}
+ >
+
{t('scoreboard.no-data')}
@@ -117,9 +121,12 @@ export const MobileScoreboard: React.FC = ({
>
{entry.team.name}
-
+
#{entry.team.number}
@@ -128,17 +135,19 @@ export const MobileScoreboard: React.FC = ({
+ color: 'text.secondary',
+ fontSize: '0.75rem'
+ }}
+ >
{t('scoreboard.best-score')}
+ color: 'primary.main'
+ }}
+ >
{entry.maxScore ?? '-'}
@@ -151,17 +160,19 @@ export const MobileScoreboard: React.FC = ({
+ }}
+ >
{t('scoreboard.match-scores')}
+ }}
+ >
{Array.from({ length: matchesPerTeam }, (_, index) => {
const score = entry.scores?.[index];
return (
@@ -181,14 +192,18 @@ export const MobileScoreboard: React.FC = ({
+ display: 'block',
+ fontSize: '0.7rem'
+ }}
+ >
{t('scoreboard.match')} {index + 1}
-
+
{score ?? '-'}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/teams-tab.tsx b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/teams-tab.tsx
index 11d3afe11..2481edbc7 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/components/tabs/teams-tab.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/components/tabs/teams-tab.tsx
@@ -51,19 +51,31 @@ export const TeamsTab: React.FC = () => {
- {t('team')}
+
+ {t('team')}
+
- {t('region')}
+
+ {t('region')}
+
- {t('location')}
+
+ {t('location')}
+
diff --git a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/event-summary.tsx b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/event-summary.tsx
index 3125f84ea..d5cda537d 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/event-summary.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/event-summary.tsx
@@ -40,9 +40,10 @@ export const EventSummary: React.FC = () => {
+ }}
+ >
{t('performance.event-summary')}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/match-results.tsx b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/match-results.tsx
index 394277907..6c1f5917e 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/match-results.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/match-results.tsx
@@ -18,9 +18,10 @@ export const MatchResults: React.FC = ({ scores }) => {
+ }}
+ >
{t('performance.match-results')}
@@ -39,14 +40,21 @@ export const MatchResults: React.FC = ({ scores }) => {
borderColor: 'grey.200'
}}
>
-
+
{t('performance.match-number', { number: index + 1 })}
-
+
{score ?? '-'}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/performance.tsx b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/performance.tsx
index 542f27263..a76462b0d 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/performance.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/event-summary/performance.tsx
@@ -31,19 +31,30 @@ export const PerformanceMetrics: React.FC = ({
borderColor: 'grey.200'
}}
>
-
+
-
+
{t('performance.highest-score')}
-
+
{highestScore ?? '-'}
@@ -60,19 +71,30 @@ export const PerformanceMetrics: React.FC = ({
borderColor: 'grey.200'
}}
>
-
+
-
+
{t('performance.robot-game-rank')}
-
+
{robotGameRank ?? '-'}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-info-header.tsx b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-info-header.tsx
index 7e56b066e..a4704d481 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-info-header.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-info-header.tsx
@@ -28,9 +28,10 @@ export const TeamInfoHeader: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 2
- }}>
+ }}
+ >
{
>
{t('header.back-to-event')}
-
+
{event.name} {division.name && `• ${division.name}`}
@@ -52,10 +56,11 @@ export const TeamInfoHeader: React.FC = () => {
component={Link}
href={`/teams/${team.slug}`}
sx={{
- alignItems: "center",
+ alignItems: 'center',
textDecoration: 'none',
color: 'inherit'
- }}>
+ }}
+ >
{
+ }}
+ >
{team.name} #{team.number}
@@ -77,13 +83,17 @@ export const TeamInfoHeader: React.FC = () => {
direction="row"
spacing={1}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mt: 1
- }}>
+ }}
+ >
-
+
{team.city} • {team.affiliation}
diff --git a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-schedule.tsx b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-schedule.tsx
index e0b2981b9..ec0d3f3fe 100644
--- a/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-schedule.tsx
+++ b/apps/portal/src/app/[locale]/event/[slug]/team/[teamSlug]/components/team-schedule.tsx
@@ -78,21 +78,28 @@ export const TeamSchedule: React.FC = () => {
direction="row"
spacing={1}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 2
- }}>
+ }}
+ >
-
+
{t('schedule.title')}
{scheduleEntries.length === 0 && (
-
+
{t('schedule.no-schedule')}
@@ -113,9 +120,12 @@ export const TeamSchedule: React.FC = () => {
-
+
{dayjs(entry.time).format('HH:mm')}
{
mx: 2
}}
/>
-
+
{entry.description}
diff --git a/apps/portal/src/app/[locale]/events/components/event-list-item.tsx b/apps/portal/src/app/[locale]/events/components/event-list-item.tsx
index 0ac34ba4b..3e51a5eeb 100644
--- a/apps/portal/src/app/[locale]/events/components/event-list-item.tsx
+++ b/apps/portal/src/app/[locale]/events/components/event-list-item.tsx
@@ -92,23 +92,26 @@ export const EventListItem: React.FC = ({ event, variant = '
direction={{ xs: 'column', sm: 'row' }}
spacing={2}
sx={{
- justifyContent: "space-between",
+ justifyContent: 'space-between',
alignItems: { xs: 'flex-start', sm: 'center' }
- }}>
+ }}
+ >
+ }}
+ >
+ }}
+ >
{event.name}
{!event.official && (
@@ -128,26 +131,38 @@ export const EventListItem: React.FC = ({ event, variant = '
spacing={{ xs: 1, sm: 3 }}
sx={{ color: 'text.secondary' }}
>
-
+
{new Date(event.startDate).toLocaleDateString()}
-
+
{event.location}
-
+
{tEvents('teams-registered', { count: event.teamsRegistered })}
diff --git a/apps/portal/src/app/[locale]/events/components/event-list-section.tsx b/apps/portal/src/app/[locale]/events/components/event-list-section.tsx
index dc4035691..204848b9e 100644
--- a/apps/portal/src/app/[locale]/events/components/event-list-section.tsx
+++ b/apps/portal/src/app/[locale]/events/components/event-list-section.tsx
@@ -36,17 +36,22 @@ export const EventsListSection: React.FC = ({
return (
-
+
{t('no-events.title')}
+ }}
+ >
{searchValue ? t('no-events.search-message') : t('no-events.filter-message')}
diff --git a/apps/portal/src/app/[locale]/events/components/event-section.tsx b/apps/portal/src/app/[locale]/events/components/event-section.tsx
index 267204ffb..a462b631d 100644
--- a/apps/portal/src/app/[locale]/events/components/event-section.tsx
+++ b/apps/portal/src/app/[locale]/events/components/event-section.tsx
@@ -21,12 +21,13 @@ export const EventSection: React.FC = ({ events, variant }) =
+ }}
+ >
{t(`filters.${variant}`, { count: filteredEvents.length })}
diff --git a/apps/portal/src/app/[locale]/events/components/events-page-header.tsx b/apps/portal/src/app/[locale]/events/components/events-page-header.tsx
index 0c69e2da6..1f54f4588 100644
--- a/apps/portal/src/app/[locale]/events/components/events-page-header.tsx
+++ b/apps/portal/src/app/[locale]/events/components/events-page-header.tsx
@@ -36,10 +36,11 @@ export const EventsPageHeader: React.FC = ({
variant="h3"
component="h1"
sx={{
- fontWeight: "bold",
+ fontWeight: 'bold',
mb: 4,
fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' }
- }}>
+ }}
+ >
{{tags => t.rich('title', tags)}}
diff --git a/apps/portal/src/app/[locale]/events/components/events-search-section.tsx b/apps/portal/src/app/[locale]/events/components/events-search-section.tsx
index b822770f2..8bbcce3b7 100644
--- a/apps/portal/src/app/[locale]/events/components/events-search-section.tsx
+++ b/apps/portal/src/app/[locale]/events/components/events-search-section.tsx
@@ -59,9 +59,14 @@ export const EventsSearchSection: React.FC = ({
}}
/>
-
+
onFilterChange(EventFilter.ALL)}
diff --git a/apps/portal/src/app/[locale]/page.tsx b/apps/portal/src/app/[locale]/page.tsx
index 06fa72c5d..a840be8ba 100644
--- a/apps/portal/src/app/[locale]/page.tsx
+++ b/apps/portal/src/app/[locale]/page.tsx
@@ -23,9 +23,13 @@ export default function HomePage() {
-
+
diff --git a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/season-selector.tsx b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/season-selector.tsx
index 4b2959439..ce402d7cb 100644
--- a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/season-selector.tsx
+++ b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/season-selector.tsx
@@ -31,9 +31,11 @@ export const SeasonSelector: React.FC = ({ currentSeason })
};
return (
-
+
@@ -203,9 +234,10 @@ export const TeamEventResultCard: React.FC = ({ eventR
+ }}
+ >
{t('match-results')}
@@ -224,14 +256,21 @@ export const TeamEventResultCard: React.FC = ({ eventR
borderColor: 'grey.200'
}}
>
-
+
{t('match-number', { number: match.number })}
-
+
{match.score}
diff --git a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/team-header.tsx b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/team-header.tsx
index 6dd068244..3e745c7a0 100644
--- a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/team-header.tsx
+++ b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/team-header.tsx
@@ -19,9 +19,10 @@ export const TeamHeader: React.FC = () => {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
mb: 2
- }}>
+ }}
+ >
{
sx={{ width: 72, height: 72, objectFit: 'cover' }}
/>
-
-
+
+
{t('title', { number: team.number, name: team.name })}
@@ -43,31 +52,43 @@ export const TeamHeader: React.FC = () => {
-
+
-
+
{t('info.from', { city: team.city })}
+ }}
+ >
{team.affiliation}
{team.lastCompetedSeason && (
-
+
{t('info.last-competed', { season: team.lastCompetedSeason.name })}
diff --git a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/unpublished-event-card.tsx b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/unpublished-event-card.tsx
index a054aed88..11b379aa4 100644
--- a/apps/portal/src/app/[locale]/teams/[teamSlug]/components/unpublished-event-card.tsx
+++ b/apps/portal/src/app/[locale]/teams/[teamSlug]/components/unpublished-event-card.tsx
@@ -57,16 +57,25 @@ export const UnpublishedEventCard: React.FC = ({ even
-
-
+ }}
+ >
+
+
{eventResult.eventName}
{isDuringEvent && (
@@ -91,9 +100,12 @@ export const UnpublishedEventCard: React.FC = ({ even
{getButtonText()}
-
+
{getStatusText()}
diff --git a/apps/portal/src/app/[locale]/teams/[teamSlug]/error.tsx b/apps/portal/src/app/[locale]/teams/[teamSlug]/error.tsx
index cac2f7770..a95d5ad02 100644
--- a/apps/portal/src/app/[locale]/teams/[teamSlug]/error.tsx
+++ b/apps/portal/src/app/[locale]/teams/[teamSlug]/error.tsx
@@ -12,17 +12,22 @@ export default function TeamError() {
-
+
{t('not-found.title')}
+ }}
+ >
{t('not-found.description')}
}>
diff --git a/apps/portal/src/app/[locale]/teams/components/team-list-item.tsx b/apps/portal/src/app/[locale]/teams/components/team-list-item.tsx
index 43b65aa88..ebf7efbdc 100644
--- a/apps/portal/src/app/[locale]/teams/components/team-list-item.tsx
+++ b/apps/portal/src/app/[locale]/teams/components/team-list-item.tsx
@@ -16,9 +16,14 @@ import NextLink from 'next/link';
export const TeamListItem: React.FC<{ team: Team }> = ({ team }) => {
return (
-
+
{
return (
-
+
{t('no-teams.title')}
-
+
{t('no-teams.message')}
diff --git a/apps/portal/src/app/[locale]/teams/page.tsx b/apps/portal/src/app/[locale]/teams/page.tsx
index 1a7af68fd..754f92c8c 100644
--- a/apps/portal/src/app/[locale]/teams/page.tsx
+++ b/apps/portal/src/app/[locale]/teams/page.tsx
@@ -13,10 +13,11 @@ export default async function TeamsPage() {
variant="h3"
component="h1"
sx={{
- fontWeight: "bold",
+ fontWeight: 'bold',
mb: 4,
fontSize: { xs: '1.75rem', sm: '2.25rem', md: '2.75rem' }
- }}>
+ }}
+ >
{{tags => t.rich('title', tags)}}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/feedback-row.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/feedback-row.tsx
index e37dcecf2..c6ca64e3b 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/feedback-row.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/feedback-row.tsx
@@ -61,9 +61,13 @@ export const FeedbackRow: React.FC = ({ category, disabled })
}
}}
>
- {getFeedbackTitle(field)}...
+
+ {getFeedbackTitle(field)}...
+
))}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/field-rating-row.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/field-rating-row.tsx
index 43aa6e2b9..a3d95b48a 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/field-rating-row.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/field-rating-row.tsx
@@ -73,9 +73,10 @@ export const FieldRatingRow: React.FC = ({
spacing={0.5}
direction="row"
sx={{
- alignItems: "center",
+ alignItems: 'center',
justifyContent: label ? 'flex-start' : 'center'
- }}>
+ }}
+ >
= ({
{label && (
+ }}
+ >
{label}
)}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/section-title-row.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/section-title-row.tsx
index 5bb746979..384a5dc5d 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/section-title-row.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/section-title-row.tsx
@@ -37,16 +37,21 @@ export const SectionTitleRow: React.FC = ({ sectionId, cat
}}
colSpan={4}
>
-
+
+ }}
+ >
{getSectionTitle(sectionId)}
-
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/table-header-row.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/table-header-row.tsx
index 664d2b10e..af832355f 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/table-header-row.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/desktop/table-header-row.tsx
@@ -40,9 +40,10 @@ export const TableHeaderRow: React.FC = ({ category }) => {
>
+ }}
+ >
{getColumnTitle(column)}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/judging-timer.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/judging-timer.tsx
index caa4e401c..e174475dc 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/judging-timer.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/judging-timer.tsx
@@ -215,24 +215,30 @@ export const JudgingTimer = () => {
+ }}
+ >
{formatTime(totalTimeRemaining)}
+ }}
+ >
-
+
{JUDGING_STAGES.map((stage: { id: string; duration: number }, index: number) => {
let progressPercentage = 0;
@@ -281,10 +287,11 @@ export const JudgingTimer = () => {
+ }}
+ >
{getStage(JUDGING_STAGES[currentStage].id)}
@@ -293,12 +300,16 @@ export const JudgingTimer = () => {
direction={{ xs: 'column', md: 'row' }}
spacing={1}
sx={{
- justifyContent: "space-between"
+ justifyContent: 'space-between'
}}
>
-
+
back()}
@@ -339,9 +350,13 @@ export const JudgingTimer = () => {
-
+
= ({ category, disabl
sx={{
fontWeight: 700,
color
- }}>
+ }}
+ >
{t('mobile-feedback')}
}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-field-section.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-field-section.tsx
index 0b61c4e5a..1afbfa478 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-field-section.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-field-section.tsx
@@ -84,9 +84,12 @@ export const MobileFieldSection: React.FC = ({
+
{getCurrentLevelLabel()}
}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-section.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-section.tsx
index 3d84480df..6bb0388fe 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-section.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/mobile/mobile-section.tsx
@@ -53,7 +53,8 @@ export const MobileSection: React.FC = ({
sx={{
fontWeight: 700,
color: color
- }}>
+ }}
+ >
{getSectionTitle(sectionId)}
}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/components/rubric-header.tsx b/apps/portal/src/app/[locale]/tools/rubrics/components/rubric-header.tsx
index 4e5ac8e35..ddb947248 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/components/rubric-header.tsx
+++ b/apps/portal/src/app/[locale]/tools/rubrics/components/rubric-header.tsx
@@ -12,12 +12,17 @@ export const RubricHeader = () => {
-
+ justifyContent: 'space-between',
+ alignItems: 'center'
+ }}
+ >
+
{t('title')}
diff --git a/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts b/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts
index dc2b72de7..ad1cf7190 100644
--- a/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts
+++ b/apps/portal/src/app/[locale]/tools/rubrics/hooks/use-judging-timer.ts
@@ -131,7 +131,7 @@ const createTimerReducer = (stages: ReturnType) => {
export const useJudgingTimer = (): [JudgingTimerState, JudgingTimerControls] => {
const locale = useLocale();
const { playSound } = useJudgingSounds();
-
+
const JUDGING_STAGES = useMemo(() => getJudgingStages(locale), [locale]);
const timerReducer = useMemo(() => createTimerReducer(JUDGING_STAGES), [JUDGING_STAGES]);
diff --git a/apps/portal/src/app/[locale]/tools/scorer/components/field-timer.tsx b/apps/portal/src/app/[locale]/tools/scorer/components/field-timer.tsx
index 4206b6609..425ca110f 100644
--- a/apps/portal/src/app/[locale]/tools/scorer/components/field-timer.tsx
+++ b/apps/portal/src/app/[locale]/tools/scorer/components/field-timer.tsx
@@ -1,7 +1,16 @@
'use client';
import { useState, useContext, useRef, useEffect } from 'react';
-import { Typography, Stack, Paper, IconButton, Slide, Fab, useTheme, useMediaQuery } from '@mui/material';
+import {
+ Typography,
+ Stack,
+ Paper,
+ IconButton,
+ Slide,
+ Fab,
+ useTheme,
+ useMediaQuery
+} from '@mui/material';
import {
Timer as TimerIcon,
PlayArrow as PlayIcon,
@@ -73,9 +82,9 @@ export const FieldTimer = () => {
if ((e.target as HTMLElement).closest('button')) {
return;
}
-
+
if (!paperRef.current) return;
-
+
const rect = paperRef.current.getBoundingClientRect();
setDragState({
isDragging: true,
@@ -191,12 +200,13 @@ export const FieldTimer = () => {
+ }}
+ >
{
direction="row"
spacing={1}
sx={{
- justifyContent: "center",
+ justifyContent: 'center',
p: 1.5
- }}>
+ }}
+ >
= ({
return (
-
+
{description}
-
+
{clause.type === 'boolean' ? (
{
direction="row"
spacing={3}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
p: 4,
position: 'fixed',
bottom: 10,
@@ -30,13 +30,15 @@ export const ScoreFloater = () => {
bgcolor: 'primary.main',
borderRadius: 4,
height: 50
- }}>
+ }}
+ >
+ }}
+ >
{t('score', { points })}
diff --git a/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-form.tsx b/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-form.tsx
index 48cf0958f..98b82b1e7 100644
--- a/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-form.tsx
+++ b/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-form.tsx
@@ -16,7 +16,8 @@ export const ScoresheetForm: React.FC = () => {
sx={{
mt: 4,
mb: 16
- }}>
+ }}
+ >
{scoresheet.missions.map((mission, index) => (
{
/>
))}
{errors.map((error, index) => (
-
+
{getError(error.id)}
))}
diff --git a/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-mission.tsx b/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-mission.tsx
index 8d0d0281e..3a8efa222 100644
--- a/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-mission.tsx
+++ b/apps/portal/src/app/[locale]/tools/scorer/components/scoresheet-mission.tsx
@@ -30,40 +30,55 @@ const ScoresheetMission: React.FC = ({ missionIndex, mis
};
return (
-
+
0 ? theme.palette.error.main : theme.palette.primary.main,
- borderRadius: "6px 0 0 0",
- textAlign: "center"
- }}>
+ borderRadius: '6px 0 0 0',
+ textAlign: 'center'
+ }}
+ >
+ }}
+ >
{mission.id.toUpperCase()}
-
-
+
+
+ }}
+ >
{title}
{mission.noEquipment && (
@@ -75,11 +90,12 @@ const ScoresheetMission: React.FC = ({ missionIndex, mis
+ }}
+ >
{description}
@@ -103,34 +119,43 @@ const ScoresheetMission: React.FC = ({ missionIndex, mis
/>
);
})}
-
+
{remarks.map((remark, index) => (
+ }}
+ >
{remark}
))}
{errors.length > 0 &&
errors.map(error => (
-
+
+ }}
+ >
{getError(error.id)}
@@ -142,7 +167,8 @@ const ScoresheetMission: React.FC = ({ missionIndex, mis
borderRadius: 8,
p: 2,
display: { xs: 'none', md: 'block' }
- }}>
+ }}
+ >
+ }}
+ >
{t('title')}
@@ -25,14 +26,19 @@ export default async function ScorerPage() {
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "center"
- }}>
+ alignItems: 'center',
+ justifyContent: 'center'
+ }}
+ >
- {t('no-equipment-constraint-title')}
+
+ {t('no-equipment-constraint-title')}
+
{t('no-equipment-constraint')}
diff --git a/apps/portal/tsconfig.json b/apps/portal/tsconfig.json
index 9a7b281b3..54ea19f7f 100644
--- a/apps/portal/tsconfig.json
+++ b/apps/portal/tsconfig.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"jsx": "preserve",
"strict": true,
+ "types": ["node", "@types/grecaptcha"],
"noEmit": true,
"emitDeclarationOnly": false,
"esModuleInterop": true,
@@ -12,7 +13,6 @@
"isolatedModules": true,
"lib": [
"dom",
- "dom.iterable",
"esnext"
],
"allowJs": true,
diff --git a/libs/database/src/migrations/010_create_robot_game_tables_matches_and_participants.ts b/libs/database/src/migrations/010_create_robot_game_tables_matches_and_participants.ts
index ae68902d5..7777eebf5 100644
--- a/libs/database/src/migrations/010_create_robot_game_tables_matches_and_participants.ts
+++ b/libs/database/src/migrations/010_create_robot_game_tables_matches_and_participants.ts
@@ -225,7 +225,7 @@ export async function up(db: Kysely): Promise {
export async function down(db: Kysely): Promise {
// Drop partial unique index for team-match assignments
await sql`DROP INDEX IF EXISTS uk_robot_game_match_participants_team_match_not_null`.execute(db);
-
+
// Drop indexes for robot_game_match_participants table
await db.schema.dropIndex('idx_robot_game_match_participants_team_id').ifExists().execute();
await db.schema.dropIndex('idx_robot_game_match_participants_table_id').ifExists().execute();
diff --git a/libs/database/src/migrations/015_add_arrived_at_to_team_divisions.ts b/libs/database/src/migrations/015_add_arrived_at_to_team_divisions.ts
index cbbaf86f8..9867d29b6 100644
--- a/libs/database/src/migrations/015_add_arrived_at_to_team_divisions.ts
+++ b/libs/database/src/migrations/015_add_arrived_at_to_team_divisions.ts
@@ -3,16 +3,10 @@ import { Kysely } from 'kysely';
export async function up(db: Kysely): Promise {
// Add arrivedAt field to team_divisions table
- await db.schema
- .alterTable('team_divisions')
- .addColumn('arrived_at', 'timestamptz')
- .execute();
+ await db.schema.alterTable('team_divisions').addColumn('arrived_at', 'timestamptz').execute();
}
export async function down(db: Kysely): Promise {
// Drop the arrivedAt field
- await db.schema
- .alterTable('team_divisions')
- .dropColumn('arrived_at')
- .execute();
+ await db.schema.alterTable('team_divisions').dropColumn('arrived_at').execute();
}
diff --git a/libs/database/src/migrations/024_add_location_to_agenda_events.ts b/libs/database/src/migrations/024_add_location_to_agenda_events.ts
index be7dcbcb1..1894063ac 100644
--- a/libs/database/src/migrations/024_add_location_to_agenda_events.ts
+++ b/libs/database/src/migrations/024_add_location_to_agenda_events.ts
@@ -2,15 +2,9 @@
import { Kysely } from 'kysely';
export async function up(db: Kysely): Promise {
- await db.schema
- .alterTable('agenda_events')
- .addColumn('location', 'text')
- .execute();
+ await db.schema.alterTable('agenda_events').addColumn('location', 'text').execute();
}
export async function down(db: Kysely): Promise {
- await db.schema
- .alterTable('agenda_events')
- .dropColumn('location')
- .execute();
+ await db.schema.alterTable('agenda_events').dropColumn('location').execute();
}
diff --git a/libs/database/src/repositories/robot-game-matches.ts b/libs/database/src/repositories/robot-game-matches.ts
index aec322ff3..3b4d608f5 100644
--- a/libs/database/src/repositories/robot-game-matches.ts
+++ b/libs/database/src/repositories/robot-game-matches.ts
@@ -284,11 +284,8 @@ export class RobotGameMatchesRepository {
.selectFrom('robot_game_match_participants')
.selectAll()
.where('team_id', '=', teamId1)
- .where('match_id', 'in', (eb) =>
- eb
- .selectFrom('robot_game_matches')
- .select('id')
- .where('division_id', '=', divisionId)
+ .where('match_id', 'in', eb =>
+ eb.selectFrom('robot_game_matches').select('id').where('division_id', '=', divisionId)
)
.execute();
@@ -296,11 +293,8 @@ export class RobotGameMatchesRepository {
.selectFrom('robot_game_match_participants')
.selectAll()
.where('team_id', '=', teamId2)
- .where('match_id', 'in', (eb) =>
- eb
- .selectFrom('robot_game_matches')
- .select('id')
- .where('division_id', '=', divisionId)
+ .where('match_id', 'in', eb =>
+ eb.selectFrom('robot_game_matches').select('id').where('division_id', '=', divisionId)
)
.execute();
diff --git a/libs/database/src/repositories/rooms.ts b/libs/database/src/repositories/rooms.ts
index 2d69b6222..d702179f7 100644
--- a/libs/database/src/repositories/rooms.ts
+++ b/libs/database/src/repositories/rooms.ts
@@ -35,7 +35,7 @@ class RoomSelector {
return updatedRoom;
}
-
+
async delete(): Promise {
await this.db.deleteFrom('judging_rooms').where('id', '=', this.id).execute();
}
@@ -83,11 +83,11 @@ export class RoomsRepository {
.values(newRoom)
.returningAll()
.execute();
-
+
if (!createdRoom) {
throw new Error('Failed to create room');
}
-
+
return createdRoom;
}
}
diff --git a/libs/database/src/repositories/rubrics.ts b/libs/database/src/repositories/rubrics.ts
index 7a3e53f5d..b803cb411 100644
--- a/libs/database/src/repositories/rubrics.ts
+++ b/libs/database/src/repositories/rubrics.ts
@@ -36,8 +36,7 @@ class RubricSelector {
}
type RubricsSelectorType =
- | { type: 'division'; divisionId: string }
- | { type: 'room'; roomId: string };
+ { type: 'division'; divisionId: string } | { type: 'room'; roomId: string };
class RubricsSelector {
constructor(
diff --git a/libs/database/src/repositories/scoresheets.ts b/libs/database/src/repositories/scoresheets.ts
index bcb0442bb..fddbf457b 100644
--- a/libs/database/src/repositories/scoresheets.ts
+++ b/libs/database/src/repositories/scoresheets.ts
@@ -36,8 +36,7 @@ class ScoresheetSelector {
}
type ScoresheetsSelectorType =
- | { type: 'division'; divisionId: string }
- | { type: 'table'; tableId: string };
+ { type: 'division'; divisionId: string } | { type: 'table'; tableId: string };
class ScoresheetsSelector {
constructor(
diff --git a/libs/database/src/repositories/tables.ts b/libs/database/src/repositories/tables.ts
index a8e77b632..1ab582933 100644
--- a/libs/database/src/repositories/tables.ts
+++ b/libs/database/src/repositories/tables.ts
@@ -83,11 +83,11 @@ export class TablesRepository {
.values(newTable)
.returningAll()
.execute();
-
+
if (!createdTable) {
throw new Error('Failed to create table');
}
-
+
return createdTable;
}
}
diff --git a/libs/database/src/schema/documents/division-state.ts b/libs/database/src/schema/documents/division-state.ts
index 588fc2025..2e3741167 100644
--- a/libs/database/src/schema/documents/division-state.ts
+++ b/libs/database/src/schema/documents/division-state.ts
@@ -1,10 +1,5 @@
export type AudienceDisplayScreen =
- | 'scoreboard'
- | 'match_preview'
- | 'sponsors'
- | 'logo'
- | 'message'
- | 'awards';
+ 'scoreboard' | 'match_preview' | 'sponsors' | 'logo' | 'message' | 'awards';
export interface AwardsPresentation {
slideIndex: number;
diff --git a/libs/localization/src/lib/locale/he.json b/libs/localization/src/lib/locale/he.json
index 17f60a65f..c5dfdc342 100644
--- a/libs/localization/src/lib/locale/he.json
+++ b/libs/localization/src/lib/locale/he.json
@@ -592,4 +592,4 @@
"think-about": "חשבו על..."
}
}
-}
\ No newline at end of file
+}
diff --git a/libs/localization/src/lib/locale/pl.json b/libs/localization/src/lib/locale/pl.json
index a3b59d257..eaead479d 100644
--- a/libs/localization/src/lib/locale/pl.json
+++ b/libs/localization/src/lib/locale/pl.json
@@ -332,8 +332,7 @@
}
}
}
- }
- ,
+ },
"errors": {
"e1": {
"description": "Pędzel nie może znajdować się jednocześnie w forum (M14) i dotykać stanowiska wykopalisk (M01)."
diff --git a/libs/localization/tsconfig.json b/libs/localization/tsconfig.json
index 95cfeb243..95cac5127 100644
--- a/libs/localization/tsconfig.json
+++ b/libs/localization/tsconfig.json
@@ -2,7 +2,6 @@
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
- "esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
diff --git a/libs/presentations/src/lib/components/logo-stack.tsx b/libs/presentations/src/lib/components/logo-stack.tsx
index 6a6a76016..9982179b6 100644
--- a/libs/presentations/src/lib/components/logo-stack.tsx
+++ b/libs/presentations/src/lib/components/logo-stack.tsx
@@ -13,8 +13,8 @@ export const LogoStack: React.FC = ({ color }) => {
= ({ color }) => {
height: '100px',
bgcolor: '#f7f8f9',
borderTop: `10px solid ${color || 'transparent'}`
- }}>
+ }}
+ >
= ({ awards
direction="column"
spacing={5}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
height: '100%',
width: '100%',
px: 4,
textAlign: 'center',
position: 'relative'
- }}>
+ }}
+ >
{t('advancing-teams')}
@@ -98,7 +99,7 @@ export const AdvancingTeamsSlide: React.FC = ({ awards
direction="column"
spacing={2}
sx={{
- alignItems: "center",
+ alignItems: 'center',
p: 3,
borderRadius: 2,
backgroundColor: '#f9fafb',
@@ -111,7 +112,8 @@ export const AdvancingTeamsSlide: React.FC = ({ awards
transform: 'translateY(-4px)',
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.1)'
}
- }}>
+ }}
+ >
= ({ award }
direction="row"
spacing={2}
sx={{
- alignItems: "center",
- justifyContent: "center"
- }}>
+ alignItems: 'center',
+ justifyContent: 'center'
+ }}
+ >
= ({ award }) =>
direction="column"
spacing={3}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
height: '100%',
width: '100%',
px: 4,
textAlign: 'center',
position: 'relative'
- }}>
+ }}
+ >
{localizedAwardName}
diff --git a/libs/presentations/src/lib/components/slides/title-slide.tsx b/libs/presentations/src/lib/components/slides/title-slide.tsx
index b3b828a4b..f71aa567b 100644
--- a/libs/presentations/src/lib/components/slides/title-slide.tsx
+++ b/libs/presentations/src/lib/components/slides/title-slide.tsx
@@ -18,14 +18,15 @@ export const TitleSlide: React.FC = ({ primary, secondary, divi
direction="column"
spacing={6}
sx={{
- alignItems: "center",
- justifyContent: "center",
+ alignItems: 'center',
+ justifyContent: 'center',
height: '100%',
width: '100%',
px: 4,
textAlign: 'center',
position: 'relative'
- }}>
+ }}
+ >
= ({
sx = {}
}) => {
const theme = useTheme();
- const [open, setOpen] = useState(false);
+ const [open, setOpen] = useState(defaultOpen);
const [hexInput, setHexInput] = useState(hsvaToHex(value));
const [anchorEl, setAnchorEl] = useState(null);
+ const [prevValue, setPrevValue] = useState(value);
+ const [prevDefaultOpen, setPrevDefaultOpen] = useState(defaultOpen);
- useEffect(() => {
+ if (value !== prevValue) {
+ setPrevValue(value);
setHexInput(hsvaToHex(value));
- }, [value]);
+ }
- useEffect(() => {
- if (defaultOpen) setOpen(true);
- }, [defaultOpen]);
+ if (defaultOpen !== prevDefaultOpen) {
+ setPrevDefaultOpen(defaultOpen);
+ setOpen(defaultOpen);
+ }
const handleSaturationChange = (newHsva: HsvaColor) => {
onChange(newHsva);
@@ -100,9 +104,12 @@ export const ColorPicker: React.FC = ({
};
return (
-
+
= ({
+ overflow: 'hidden'
+ }}
+ >
= ({
+ }}
+ >
= ({
{/* Preset color swatches */}
+ }}
+ >
{PRESET_COLORS.map(hex => (
= ({
+ }}
+ />
diff --git a/libs/shared/src/lib/components/file-upload.tsx b/libs/shared/src/lib/components/file-upload.tsx
index 4c639208d..dffaf6fd4 100644
--- a/libs/shared/src/lib/components/file-upload.tsx
+++ b/libs/shared/src/lib/components/file-upload.tsx
@@ -48,9 +48,10 @@ export const FileUpload: React.FC = ({
component="span"
variant="body2"
sx={{
- color: "text.secondary",
+ color: 'text.secondary',
ml: 1
- }}>
+ }}
+ >
({description})
)}
diff --git a/libs/shared/src/lib/components/flag.tsx b/libs/shared/src/lib/components/flag.tsx
index 395eae98a..a2142add6 100644
--- a/libs/shared/src/lib/components/flag.tsx
+++ b/libs/shared/src/lib/components/flag.tsx
@@ -1,8 +1,10 @@
import Image from 'next/image';
import { getRegionFlagUrl } from '../utils';
-interface FlagProps
- extends Omit, 'src' | 'alt' | 'width' | 'height' | 'fill'> {
+interface FlagProps extends Omit<
+ React.ComponentProps,
+ 'src' | 'alt' | 'width' | 'height' | 'fill'
+> {
region: string;
size: number;
}
diff --git a/libs/shared/src/lib/components/formik/formik-conditional-text-field.tsx b/libs/shared/src/lib/components/formik/formik-conditional-text-field.tsx
index 6a93e8132..1e0f66f31 100644
--- a/libs/shared/src/lib/components/formik/formik-conditional-text-field.tsx
+++ b/libs/shared/src/lib/components/formik/formik-conditional-text-field.tsx
@@ -22,9 +22,12 @@ export const FormikConditionalTextField: React.FC
{({ field, form }: FieldProps) => (
-
+
{
+export interface NumberInputProps extends Omit<
+ TextFieldProps,
+ 'onChange' | 'value' | 'type' | 'disabled'
+> {
value: number | null;
onChange: (event: React.MouseEvent | React.ChangeEvent, value: number | null) => void;
min?: number;
@@ -51,12 +53,19 @@ export const NumberInput = React.forwardRef(function CustomNumberInput(
};
return (
-
-
+
+
handleStep(e, 'decrement')}
disabled={disabled || value === null || value <= min}
@@ -110,10 +119,11 @@ export const NumberInput = React.forwardRef(function CustomNumberInput(
+ }}
+ >
{helperText}
diff --git a/libs/shared/src/lib/components/responsive-component.tsx b/libs/shared/src/lib/components/responsive-component.tsx
index 22335ce1c..d8e8fb463 100644
--- a/libs/shared/src/lib/components/responsive-component.tsx
+++ b/libs/shared/src/lib/components/responsive-component.tsx
@@ -16,16 +16,24 @@ export const ResponsiveComponent = ({
<>
+ sx={[
+ {
+ display: { xs: 'block', [mobileBreakpoint]: 'none' }
+ },
+ ...(Array.isArray(boxProps.sx) ? boxProps.sx : [boxProps.sx])
+ ]}
+ >
{mobile}
+ sx={[
+ {
+ display: { xs: 'none', [mobileBreakpoint]: 'block' }
+ },
+ ...(Array.isArray(boxProps.sx) ? boxProps.sx : [boxProps.sx])
+ ]}
+ >
{desktop}
>
diff --git a/libs/shared/src/lib/consts.ts b/libs/shared/src/lib/consts.ts
index 27e4c360f..4d58821cf 100644
--- a/libs/shared/src/lib/consts.ts
+++ b/libs/shared/src/lib/consts.ts
@@ -2,4 +2,4 @@ const MATCH_LOAD_THRESHOLD = 15;
const MATCH_START_THRESHOLD = 5;
const SESSION_START_THRESOLD = 5;
-export { MATCH_LOAD_THRESHOLD, SESSION_START_THRESOLD, MATCH_START_THRESHOLD};
\ No newline at end of file
+export { MATCH_LOAD_THRESHOLD, SESSION_START_THRESOLD, MATCH_START_THRESHOLD };
diff --git a/libs/shared/src/lib/hooks/use-audio-player.ts b/libs/shared/src/lib/hooks/use-audio-player.ts
index 1deb27bb5..634d4019d 100644
--- a/libs/shared/src/lib/hooks/use-audio-player.ts
+++ b/libs/shared/src/lib/hooks/use-audio-player.ts
@@ -24,9 +24,7 @@ export interface AudioPlayerOptions {
* // Safe to call even if audio isn't ready
* playSound('start');
*/
-export const useAudioPlayer = (
- options: AudioPlayerOptions
-) => {
+export const useAudioPlayer = (options: AudioPlayerOptions) => {
const { sounds, preload = 'auto' } = options;
const soundRefs = useRef>(
{} as Record
@@ -52,7 +50,7 @@ export const useAudioPlayer = (
audio.preload = preload;
soundRefs.current[key as T] = audio;
});
- setIsReady(true);
+ queueMicrotask(() => setIsReady(true));
} catch (error) {
console.error('Failed to initialize audio:', error);
}
@@ -66,7 +64,7 @@ export const useAudioPlayer = (
}
});
soundRefs.current = {} as Record;
- setIsReady(false);
+ queueMicrotask(() => setIsReady(false));
};
}, [sounds, preload]);
diff --git a/libs/shared/src/lib/utils/timezones.ts b/libs/shared/src/lib/utils/timezones.ts
index 4a8afc46b..2366153d7 100644
--- a/libs/shared/src/lib/utils/timezones.ts
+++ b/libs/shared/src/lib/utils/timezones.ts
@@ -1,594 +1,594 @@
export const ALL_TIMEZONES = [
-'Africa/Abidjan',
-'Africa/Accra',
-'Africa/Addis_Ababa',
-'Africa/Algiers',
-'Africa/Asmara',
-'Africa/Asmera',
-'Africa/Bamako',
-'Africa/Bangui',
-'Africa/Banjul',
-'Africa/Bissau',
-'Africa/Blantyre',
-'Africa/Brazzaville',
-'Africa/Bujumbura',
-'Africa/Cairo',
-'Africa/Casablanca',
-'Africa/Ceuta',
-'Africa/Conakry',
-'Africa/Dakar',
-'Africa/Dar_es_Salaam',
-'Africa/Djibouti',
-'Africa/Douala',
-'Africa/El_Aaiun',
-'Africa/Freetown',
-'Africa/Gaborone',
-'Africa/Harare',
-'Africa/Johannesburg',
-'Africa/Juba',
-'Africa/Kampala',
-'Africa/Khartoum',
-'Africa/Kigali',
-'Africa/Kinshasa',
-'Africa/Lagos',
-'Africa/Libreville',
-'Africa/Lome',
-'Africa/Luanda',
-'Africa/Lubumbashi',
-'Africa/Lusaka',
-'Africa/Malabo',
-'Africa/Maputo',
-'Africa/Maseru',
-'Africa/Mbabane',
-'Africa/Mogadishu',
-'Africa/Monrovia',
-'Africa/Nairobi',
-'Africa/Ndjamena',
-'Africa/Niamey',
-'Africa/Nouakchott',
-'Africa/Ouagadougou',
-'Africa/Porto-Novo',
-'Africa/Sao_Tome',
-'Africa/Timbuktu',
-'Africa/Tripoli',
-'Africa/Tunis',
-'Africa/Windhoek',
-'America/Adak',
-'America/Anchorage',
-'America/Anguilla',
-'America/Antigua',
-'America/Araguaina',
-'America/Argentina/Buenos_Aires',
-'America/Argentina/Catamarca',
-'America/Argentina/ComodRivadavia',
-'America/Argentina/Cordoba',
-'America/Argentina/Jujuy',
-'America/Argentina/La_Rioja',
-'America/Argentina/Mendoza',
-'America/Argentina/Rio_Gallegos',
-'America/Argentina/Salta',
-'America/Argentina/San_Juan',
-'America/Argentina/San_Luis',
-'America/Argentina/Tucuman',
-'America/Argentina/Ushuaia',
-'America/Aruba',
-'America/Asuncion',
-'America/Atikokan',
-'America/Atka',
-'America/Bahia',
-'America/Bahia_Banderas',
-'America/Barbados',
-'America/Belem',
-'America/Belize',
-'America/Blanc-Sablon',
-'America/Boa_Vista',
-'America/Bogota',
-'America/Boise',
-'America/Buenos_Aires',
-'America/Cambridge_Bay',
-'America/Campo_Grande',
-'America/Cancun',
-'America/Caracas',
-'America/Catamarca',
-'America/Cayenne',
-'America/Cayman',
-'America/Chicago',
-'America/Chihuahua',
-'America/Coral_Harbour',
-'America/Cordoba',
-'America/Costa_Rica',
-'America/Creston',
-'America/Cuiaba',
-'America/Curacao',
-'America/Danmarkshavn',
-'America/Dawson',
-'America/Dawson_Creek',
-'America/Denver',
-'America/Detroit',
-'America/Dominica',
-'America/Edmonton',
-'America/Eirunepe',
-'America/El_Salvador',
-'America/Ensenada',
-'America/Fort_Nelson',
-'America/Fort_Wayne',
-'America/Fortaleza',
-'America/Glace_Bay',
-'America/Godthab',
-'America/Goose_Bay',
-'America/Grand_Turk',
-'America/Grenada',
-'America/Guadeloupe',
-'America/Guatemala',
-'America/Guayaquil',
-'America/Guyana',
-'America/Halifax',
-'America/Havana',
-'America/Hermosillo',
-'America/Indiana/Indianapolis',
-'America/Indiana/Knox',
-'America/Indiana/Marengo',
-'America/Indiana/Petersburg',
-'America/Indiana/Tell_City',
-'America/Indiana/Vevay',
-'America/Indiana/Vincennes',
-'America/Indiana/Winamac',
-'America/Indianapolis',
-'America/Inuvik',
-'America/Iqaluit',
-'America/Jamaica',
-'America/Jujuy',
-'America/Juneau',
-'America/Kentucky/Louisville',
-'America/Kentucky/Monticello',
-'America/Knox_IN',
-'America/Kralendijk',
-'America/La_Paz',
-'America/Lima',
-'America/Los_Angeles',
-'America/Louisville',
-'America/Lower_Princes',
-'America/Maceio',
-'America/Managua',
-'America/Manaus',
-'America/Marigot',
-'America/Martinique',
-'America/Matamoros',
-'America/Mazatlan',
-'America/Mendoza',
-'America/Menominee',
-'America/Merida',
-'America/Metlakatla',
-'America/Mexico_City',
-'America/Miquelon',
-'America/Moncton',
-'America/Monterrey',
-'America/Montevideo',
-'America/Montreal',
-'America/Montserrat',
-'America/Nassau',
-'America/New_York',
-'America/Nipigon',
-'America/Nome',
-'America/Noronha',
-'America/North_Dakota/Beulah',
-'America/North_Dakota/Center',
-'America/North_Dakota/New_Salem',
-'America/Ojinaga',
-'America/Panama',
-'America/Pangnirtung',
-'America/Paramaribo',
-'America/Phoenix',
-'America/Port-au-Prince',
-'America/Port_of_Spain',
-'America/Porto_Acre',
-'America/Porto_Velho',
-'America/Puerto_Rico',
-'America/Punta_Arenas',
-'America/Rainy_River',
-'America/Rankin_Inlet',
-'America/Recife',
-'America/Regina',
-'America/Resolute',
-'America/Rio_Branco',
-'America/Rosario',
-'America/Santa_Isabel',
-'America/Santarem',
-'America/Santiago',
-'America/Santo_Domingo',
-'America/Sao_Paulo',
-'America/Scoresbysund',
-'America/Shiprock',
-'America/Sitka',
-'America/St_Barthelemy',
-'America/St_Johns',
-'America/St_Kitts',
-'America/St_Lucia',
-'America/St_Thomas',
-'America/St_Vincent',
-'America/Swift_Current',
-'America/Tegucigalpa',
-'America/Thule',
-'America/Thunder_Bay',
-'America/Tijuana',
-'America/Toronto',
-'America/Tortola',
-'America/Vancouver',
-'America/Virgin',
-'America/Whitehorse',
-'America/Winnipeg',
-'America/Yakutat',
-'America/Yellowknife',
-'Antarctica/Casey',
-'Antarctica/Davis',
-'Antarctica/DumontDUrville',
-'Antarctica/Macquarie',
-'Antarctica/Mawson',
-'Antarctica/McMurdo',
-'Antarctica/Palmer',
-'Antarctica/Rothera',
-'Antarctica/South_Pole',
-'Antarctica/Syowa',
-'Antarctica/Troll',
-'Antarctica/Vostok',
-'Arctic/Longyearbyen',
-'Asia/Aden',
-'Asia/Almaty',
-'Asia/Amman',
-'Asia/Anadyr',
-'Asia/Aqtau',
-'Asia/Aqtobe',
-'Asia/Ashgabat',
-'Asia/Ashkhabad',
-'Asia/Atyrau',
-'Asia/Baghdad',
-'Asia/Bahrain',
-'Asia/Baku',
-'Asia/Bangkok',
-'Asia/Barnaul',
-'Asia/Beirut',
-'Asia/Bishkek',
-'Asia/Brunei',
-'Asia/Calcutta',
-'Asia/Chita',
-'Asia/Choibalsan',
-'Asia/Chongqing',
-'Asia/Chungking',
-'Asia/Colombo',
-'Asia/Dacca',
-'Asia/Damascus',
-'Asia/Dhaka',
-'Asia/Dili',
-'Asia/Dubai',
-'Asia/Dushanbe',
-'Asia/Famagusta',
-'Asia/Gaza',
-'Asia/Harbin',
-'Asia/Hebron',
-'Asia/Ho_Chi_Minh',
-'Asia/Hong_Kong',
-'Asia/Hovd',
-'Asia/Irkutsk',
-'Asia/Istanbul',
-'Asia/Jakarta',
-'Asia/Jayapura',
-'Asia/Jerusalem',
-'Asia/Kabul',
-'Asia/Kamchatka',
-'Asia/Karachi',
-'Asia/Kashgar',
-'Asia/Kathmandu',
-'Asia/Katmandu',
-'Asia/Khandyga',
-'Asia/Kolkata',
-'Asia/Krasnoyarsk',
-'Asia/Kuala_Lumpur',
-'Asia/Kuching',
-'Asia/Kuwait',
-'Asia/Macao',
-'Asia/Macau',
-'Asia/Magadan',
-'Asia/Makassar',
-'Asia/Manila',
-'Asia/Muscat',
-'Asia/Nicosia',
-'Asia/Novokuznetsk',
-'Asia/Novosibirsk',
-'Asia/Omsk',
-'Asia/Oral',
-'Asia/Phnom_Penh',
-'Asia/Pontianak',
-'Asia/Pyongyang',
-'Asia/Qatar',
-'Asia/Qyzylorda',
-'Asia/Rangoon',
-'Asia/Riyadh',
-'Asia/Saigon',
-'Asia/Sakhalin',
-'Asia/Samarkand',
-'Asia/Seoul',
-'Asia/Shanghai',
-'Asia/Singapore',
-'Asia/Srednekolymsk',
-'Asia/Taipei',
-'Asia/Tashkent',
-'Asia/Tbilisi',
-'Asia/Tehran',
-'Asia/Tel_Aviv',
-'Asia/Thimbu',
-'Asia/Thimphu',
-'Asia/Tokyo',
-'Asia/Tomsk',
-'Asia/Ujung_Pandang',
-'Asia/Ulaanbaatar',
-'Asia/Ulan_Bator',
-'Asia/Urumqi',
-'Asia/Ust-Nera',
-'Asia/Vientiane',
-'Asia/Vladivostok',
-'Asia/Yakutsk',
-'Asia/Yangon',
-'Asia/Yekaterinburg',
-'Asia/Yerevan',
-'Atlantic/Azores',
-'Atlantic/Bermuda',
-'Atlantic/Canary',
-'Atlantic/Cape_Verde',
-'Atlantic/Faeroe',
-'Atlantic/Faroe',
-'Atlantic/Jan_Mayen',
-'Atlantic/Madeira',
-'Atlantic/Reykjavik',
-'Atlantic/South_Georgia',
-'Atlantic/St_Helena',
-'Atlantic/Stanley',
-'Australia/ACT',
-'Australia/Adelaide',
-'Australia/Brisbane',
-'Australia/Broken_Hill',
-'Australia/Canberra',
-'Australia/Currie',
-'Australia/Darwin',
-'Australia/Eucla',
-'Australia/Hobart',
-'Australia/LHI',
-'Australia/Lindeman',
-'Australia/Lord_Howe',
-'Australia/Melbourne',
-'Australia/NSW',
-'Australia/North',
-'Australia/Perth',
-'Australia/Queensland',
-'Australia/South',
-'Australia/Sydney',
-'Australia/Tasmania',
-'Australia/Victoria',
-'Australia/West',
-'Australia/Yancowinna',
-'Brazil/Acre',
-'Brazil/DeNoronha',
-'Brazil/East',
-'Brazil/West',
-'CET',
-'CST6CDT',
-'Canada/Atlantic',
-'Canada/Central',
-'Canada/Eastern',
-'Canada/Mountain',
-'Canada/Newfoundland',
-'Canada/Pacific',
-'Canada/Saskatchewan',
-'Canada/Yukon',
-'Chile/Continental',
-'Chile/EasterIsland',
-'Cuba',
-'EET',
-'EST',
-'EST5EDT',
-'Egypt',
-'Eire',
-'Etc/GMT',
-'Etc/GMT+0',
-'Etc/GMT+1',
-'Etc/GMT+10',
-'Etc/GMT+11',
-'Etc/GMT+12',
-'Etc/GMT+2',
-'Etc/GMT+3',
-'Etc/GMT+4',
-'Etc/GMT+5',
-'Etc/GMT+6',
-'Etc/GMT+7',
-'Etc/GMT+8',
-'Etc/GMT+9',
-'Etc/GMT-0',
-'Etc/GMT-1',
-'Etc/GMT-10',
-'Etc/GMT-11',
-'Etc/GMT-12',
-'Etc/GMT-13',
-'Etc/GMT-14',
-'Etc/GMT-2',
-'Etc/GMT-3',
-'Etc/GMT-4',
-'Etc/GMT-5',
-'Etc/GMT-6',
-'Etc/GMT-7',
-'Etc/GMT-8',
-'Etc/GMT-9',
-'Etc/GMT0',
-'Etc/Greenwich',
-'Etc/UCT',
-'Etc/UTC',
-'Etc/Universal',
-'Etc/Zulu',
-'Europe/Amsterdam',
-'Europe/Andorra',
-'Europe/Astrakhan',
-'Europe/Athens',
-'Europe/Belfast',
-'Europe/Belgrade',
-'Europe/Berlin',
-'Europe/Bratislava',
-'Europe/Brussels',
-'Europe/Bucharest',
-'Europe/Budapest',
-'Europe/Busingen',
-'Europe/Chisinau',
-'Europe/Copenhagen',
-'Europe/Dublin',
-'Europe/Gibraltar',
-'Europe/Guernsey',
-'Europe/Helsinki',
-'Europe/Isle_of_Man',
-'Europe/Istanbul',
-'Europe/Jersey',
-'Europe/Kaliningrad',
-'Europe/Kiev',
-'Europe/Kirov',
-'Europe/Lisbon',
-'Europe/Ljubljana',
-'Europe/London',
-'Europe/Luxembourg',
-'Europe/Madrid',
-'Europe/Malta',
-'Europe/Mariehamn',
-'Europe/Minsk',
-'Europe/Monaco',
-'Europe/Moscow',
-'Europe/Nicosia',
-'Europe/Oslo',
-'Europe/Paris',
-'Europe/Podgorica',
-'Europe/Prague',
-'Europe/Riga',
-'Europe/Rome',
-'Europe/Samara',
-'Europe/San_Marino',
-'Europe/Sarajevo',
-'Europe/Saratov',
-'Europe/Simferopol',
-'Europe/Skopje',
-'Europe/Sofia',
-'Europe/Stockholm',
-'Europe/Tallinn',
-'Europe/Tirane',
-'Europe/Tiraspol',
-'Europe/Ulyanovsk',
-'Europe/Uzhgorod',
-'Europe/Vaduz',
-'Europe/Vatican',
-'Europe/Vienna',
-'Europe/Vilnius',
-'Europe/Volgograd',
-'Europe/Warsaw',
-'Europe/Zagreb',
-'Europe/Zaporozhye',
-'Europe/Zurich',
-'GB',
-'GB-Eire',
-'GMT',
-'GMT+0',
-'GMT-0',
-'GMT0',
-'Greenwich',
-'HST',
-'Hongkong',
-'Iceland',
-'Indian/Antananarivo',
-'Indian/Chagos',
-'Indian/Christmas',
-'Indian/Cocos',
-'Indian/Comoro',
-'Indian/Kerguelen',
-'Indian/Mahe',
-'Indian/Maldives',
-'Indian/Mauritius',
-'Indian/Mayotte',
-'Indian/Reunion',
-'Iran',
-'Israel',
-'Jamaica',
-'Japan',
-'Kwajalein',
-'Libya',
-'MET',
-'MST',
-'MST7MDT',
-'Mexico/BajaNorte',
-'Mexico/BajaSur',
-'Mexico/General',
-'NZ',
-'NZ-CHAT',
-'Navajo',
-'PRC',
-'PST8PDT',
-'Pacific/Apia',
-'Pacific/Auckland',
-'Pacific/Bougainville',
-'Pacific/Chatham',
-'Pacific/Chuuk',
-'Pacific/Easter',
-'Pacific/Efate',
-'Pacific/Enderbury',
-'Pacific/Fakaofo',
-'Pacific/Fiji',
-'Pacific/Funafuti',
-'Pacific/Galapagos',
-'Pacific/Gambier',
-'Pacific/Guadalcanal',
-'Pacific/Guam',
-'Pacific/Honolulu',
-'Pacific/Johnston',
-'Pacific/Kiritimati',
-'Pacific/Kosrae',
-'Pacific/Kwajalein',
-'Pacific/Majuro',
-'Pacific/Marquesas',
-'Pacific/Midway',
-'Pacific/Nauru',
-'Pacific/Niue',
-'Pacific/Norfolk',
-'Pacific/Noumea',
-'Pacific/Pago_Pago',
-'Pacific/Palau',
-'Pacific/Pitcairn',
-'Pacific/Pohnpei',
-'Pacific/Ponape',
-'Pacific/Port_Moresby',
-'Pacific/Rarotonga',
-'Pacific/Saipan',
-'Pacific/Samoa',
-'Pacific/Tahiti',
-'Pacific/Tarawa',
-'Pacific/Tongatapu',
-'Pacific/Truk',
-'Pacific/Wake',
-'Pacific/Wallis',
-'Pacific/Yap',
-'Poland',
-'Portugal',
-'ROC',
-'ROK',
-'Singapore',
-'Turkey',
-'UCT',
-'US/Alaska',
-'US/Aleutian',
-'US/Arizona',
-'US/Central',
-'US/East-Indiana',
-'US/Eastern',
-'US/Hawaii',
-'US/Indiana-Starke',
-'US/Michigan',
-'US/Mountain',
-'US/Pacific',
-'US/Pacific-New',
-'US/Samoa',
-'UTC',
-'Universal',
-'W-SU',
-'WET',
-'Zulu',
-];
\ No newline at end of file
+ 'Africa/Abidjan',
+ 'Africa/Accra',
+ 'Africa/Addis_Ababa',
+ 'Africa/Algiers',
+ 'Africa/Asmara',
+ 'Africa/Asmera',
+ 'Africa/Bamako',
+ 'Africa/Bangui',
+ 'Africa/Banjul',
+ 'Africa/Bissau',
+ 'Africa/Blantyre',
+ 'Africa/Brazzaville',
+ 'Africa/Bujumbura',
+ 'Africa/Cairo',
+ 'Africa/Casablanca',
+ 'Africa/Ceuta',
+ 'Africa/Conakry',
+ 'Africa/Dakar',
+ 'Africa/Dar_es_Salaam',
+ 'Africa/Djibouti',
+ 'Africa/Douala',
+ 'Africa/El_Aaiun',
+ 'Africa/Freetown',
+ 'Africa/Gaborone',
+ 'Africa/Harare',
+ 'Africa/Johannesburg',
+ 'Africa/Juba',
+ 'Africa/Kampala',
+ 'Africa/Khartoum',
+ 'Africa/Kigali',
+ 'Africa/Kinshasa',
+ 'Africa/Lagos',
+ 'Africa/Libreville',
+ 'Africa/Lome',
+ 'Africa/Luanda',
+ 'Africa/Lubumbashi',
+ 'Africa/Lusaka',
+ 'Africa/Malabo',
+ 'Africa/Maputo',
+ 'Africa/Maseru',
+ 'Africa/Mbabane',
+ 'Africa/Mogadishu',
+ 'Africa/Monrovia',
+ 'Africa/Nairobi',
+ 'Africa/Ndjamena',
+ 'Africa/Niamey',
+ 'Africa/Nouakchott',
+ 'Africa/Ouagadougou',
+ 'Africa/Porto-Novo',
+ 'Africa/Sao_Tome',
+ 'Africa/Timbuktu',
+ 'Africa/Tripoli',
+ 'Africa/Tunis',
+ 'Africa/Windhoek',
+ 'America/Adak',
+ 'America/Anchorage',
+ 'America/Anguilla',
+ 'America/Antigua',
+ 'America/Araguaina',
+ 'America/Argentina/Buenos_Aires',
+ 'America/Argentina/Catamarca',
+ 'America/Argentina/ComodRivadavia',
+ 'America/Argentina/Cordoba',
+ 'America/Argentina/Jujuy',
+ 'America/Argentina/La_Rioja',
+ 'America/Argentina/Mendoza',
+ 'America/Argentina/Rio_Gallegos',
+ 'America/Argentina/Salta',
+ 'America/Argentina/San_Juan',
+ 'America/Argentina/San_Luis',
+ 'America/Argentina/Tucuman',
+ 'America/Argentina/Ushuaia',
+ 'America/Aruba',
+ 'America/Asuncion',
+ 'America/Atikokan',
+ 'America/Atka',
+ 'America/Bahia',
+ 'America/Bahia_Banderas',
+ 'America/Barbados',
+ 'America/Belem',
+ 'America/Belize',
+ 'America/Blanc-Sablon',
+ 'America/Boa_Vista',
+ 'America/Bogota',
+ 'America/Boise',
+ 'America/Buenos_Aires',
+ 'America/Cambridge_Bay',
+ 'America/Campo_Grande',
+ 'America/Cancun',
+ 'America/Caracas',
+ 'America/Catamarca',
+ 'America/Cayenne',
+ 'America/Cayman',
+ 'America/Chicago',
+ 'America/Chihuahua',
+ 'America/Coral_Harbour',
+ 'America/Cordoba',
+ 'America/Costa_Rica',
+ 'America/Creston',
+ 'America/Cuiaba',
+ 'America/Curacao',
+ 'America/Danmarkshavn',
+ 'America/Dawson',
+ 'America/Dawson_Creek',
+ 'America/Denver',
+ 'America/Detroit',
+ 'America/Dominica',
+ 'America/Edmonton',
+ 'America/Eirunepe',
+ 'America/El_Salvador',
+ 'America/Ensenada',
+ 'America/Fort_Nelson',
+ 'America/Fort_Wayne',
+ 'America/Fortaleza',
+ 'America/Glace_Bay',
+ 'America/Godthab',
+ 'America/Goose_Bay',
+ 'America/Grand_Turk',
+ 'America/Grenada',
+ 'America/Guadeloupe',
+ 'America/Guatemala',
+ 'America/Guayaquil',
+ 'America/Guyana',
+ 'America/Halifax',
+ 'America/Havana',
+ 'America/Hermosillo',
+ 'America/Indiana/Indianapolis',
+ 'America/Indiana/Knox',
+ 'America/Indiana/Marengo',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Tell_City',
+ 'America/Indiana/Vevay',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Indianapolis',
+ 'America/Inuvik',
+ 'America/Iqaluit',
+ 'America/Jamaica',
+ 'America/Jujuy',
+ 'America/Juneau',
+ 'America/Kentucky/Louisville',
+ 'America/Kentucky/Monticello',
+ 'America/Knox_IN',
+ 'America/Kralendijk',
+ 'America/La_Paz',
+ 'America/Lima',
+ 'America/Los_Angeles',
+ 'America/Louisville',
+ 'America/Lower_Princes',
+ 'America/Maceio',
+ 'America/Managua',
+ 'America/Manaus',
+ 'America/Marigot',
+ 'America/Martinique',
+ 'America/Matamoros',
+ 'America/Mazatlan',
+ 'America/Mendoza',
+ 'America/Menominee',
+ 'America/Merida',
+ 'America/Metlakatla',
+ 'America/Mexico_City',
+ 'America/Miquelon',
+ 'America/Moncton',
+ 'America/Monterrey',
+ 'America/Montevideo',
+ 'America/Montreal',
+ 'America/Montserrat',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Nome',
+ 'America/Noronha',
+ 'America/North_Dakota/Beulah',
+ 'America/North_Dakota/Center',
+ 'America/North_Dakota/New_Salem',
+ 'America/Ojinaga',
+ 'America/Panama',
+ 'America/Pangnirtung',
+ 'America/Paramaribo',
+ 'America/Phoenix',
+ 'America/Port-au-Prince',
+ 'America/Port_of_Spain',
+ 'America/Porto_Acre',
+ 'America/Porto_Velho',
+ 'America/Puerto_Rico',
+ 'America/Punta_Arenas',
+ 'America/Rainy_River',
+ 'America/Rankin_Inlet',
+ 'America/Recife',
+ 'America/Regina',
+ 'America/Resolute',
+ 'America/Rio_Branco',
+ 'America/Rosario',
+ 'America/Santa_Isabel',
+ 'America/Santarem',
+ 'America/Santiago',
+ 'America/Santo_Domingo',
+ 'America/Sao_Paulo',
+ 'America/Scoresbysund',
+ 'America/Shiprock',
+ 'America/Sitka',
+ 'America/St_Barthelemy',
+ 'America/St_Johns',
+ 'America/St_Kitts',
+ 'America/St_Lucia',
+ 'America/St_Thomas',
+ 'America/St_Vincent',
+ 'America/Swift_Current',
+ 'America/Tegucigalpa',
+ 'America/Thule',
+ 'America/Thunder_Bay',
+ 'America/Tijuana',
+ 'America/Toronto',
+ 'America/Tortola',
+ 'America/Vancouver',
+ 'America/Virgin',
+ 'America/Whitehorse',
+ 'America/Winnipeg',
+ 'America/Yakutat',
+ 'America/Yellowknife',
+ 'Antarctica/Casey',
+ 'Antarctica/Davis',
+ 'Antarctica/DumontDUrville',
+ 'Antarctica/Macquarie',
+ 'Antarctica/Mawson',
+ 'Antarctica/McMurdo',
+ 'Antarctica/Palmer',
+ 'Antarctica/Rothera',
+ 'Antarctica/South_Pole',
+ 'Antarctica/Syowa',
+ 'Antarctica/Troll',
+ 'Antarctica/Vostok',
+ 'Arctic/Longyearbyen',
+ 'Asia/Aden',
+ 'Asia/Almaty',
+ 'Asia/Amman',
+ 'Asia/Anadyr',
+ 'Asia/Aqtau',
+ 'Asia/Aqtobe',
+ 'Asia/Ashgabat',
+ 'Asia/Ashkhabad',
+ 'Asia/Atyrau',
+ 'Asia/Baghdad',
+ 'Asia/Bahrain',
+ 'Asia/Baku',
+ 'Asia/Bangkok',
+ 'Asia/Barnaul',
+ 'Asia/Beirut',
+ 'Asia/Bishkek',
+ 'Asia/Brunei',
+ 'Asia/Calcutta',
+ 'Asia/Chita',
+ 'Asia/Choibalsan',
+ 'Asia/Chongqing',
+ 'Asia/Chungking',
+ 'Asia/Colombo',
+ 'Asia/Dacca',
+ 'Asia/Damascus',
+ 'Asia/Dhaka',
+ 'Asia/Dili',
+ 'Asia/Dubai',
+ 'Asia/Dushanbe',
+ 'Asia/Famagusta',
+ 'Asia/Gaza',
+ 'Asia/Harbin',
+ 'Asia/Hebron',
+ 'Asia/Ho_Chi_Minh',
+ 'Asia/Hong_Kong',
+ 'Asia/Hovd',
+ 'Asia/Irkutsk',
+ 'Asia/Istanbul',
+ 'Asia/Jakarta',
+ 'Asia/Jayapura',
+ 'Asia/Jerusalem',
+ 'Asia/Kabul',
+ 'Asia/Kamchatka',
+ 'Asia/Karachi',
+ 'Asia/Kashgar',
+ 'Asia/Kathmandu',
+ 'Asia/Katmandu',
+ 'Asia/Khandyga',
+ 'Asia/Kolkata',
+ 'Asia/Krasnoyarsk',
+ 'Asia/Kuala_Lumpur',
+ 'Asia/Kuching',
+ 'Asia/Kuwait',
+ 'Asia/Macao',
+ 'Asia/Macau',
+ 'Asia/Magadan',
+ 'Asia/Makassar',
+ 'Asia/Manila',
+ 'Asia/Muscat',
+ 'Asia/Nicosia',
+ 'Asia/Novokuznetsk',
+ 'Asia/Novosibirsk',
+ 'Asia/Omsk',
+ 'Asia/Oral',
+ 'Asia/Phnom_Penh',
+ 'Asia/Pontianak',
+ 'Asia/Pyongyang',
+ 'Asia/Qatar',
+ 'Asia/Qyzylorda',
+ 'Asia/Rangoon',
+ 'Asia/Riyadh',
+ 'Asia/Saigon',
+ 'Asia/Sakhalin',
+ 'Asia/Samarkand',
+ 'Asia/Seoul',
+ 'Asia/Shanghai',
+ 'Asia/Singapore',
+ 'Asia/Srednekolymsk',
+ 'Asia/Taipei',
+ 'Asia/Tashkent',
+ 'Asia/Tbilisi',
+ 'Asia/Tehran',
+ 'Asia/Tel_Aviv',
+ 'Asia/Thimbu',
+ 'Asia/Thimphu',
+ 'Asia/Tokyo',
+ 'Asia/Tomsk',
+ 'Asia/Ujung_Pandang',
+ 'Asia/Ulaanbaatar',
+ 'Asia/Ulan_Bator',
+ 'Asia/Urumqi',
+ 'Asia/Ust-Nera',
+ 'Asia/Vientiane',
+ 'Asia/Vladivostok',
+ 'Asia/Yakutsk',
+ 'Asia/Yangon',
+ 'Asia/Yekaterinburg',
+ 'Asia/Yerevan',
+ 'Atlantic/Azores',
+ 'Atlantic/Bermuda',
+ 'Atlantic/Canary',
+ 'Atlantic/Cape_Verde',
+ 'Atlantic/Faeroe',
+ 'Atlantic/Faroe',
+ 'Atlantic/Jan_Mayen',
+ 'Atlantic/Madeira',
+ 'Atlantic/Reykjavik',
+ 'Atlantic/South_Georgia',
+ 'Atlantic/St_Helena',
+ 'Atlantic/Stanley',
+ 'Australia/ACT',
+ 'Australia/Adelaide',
+ 'Australia/Brisbane',
+ 'Australia/Broken_Hill',
+ 'Australia/Canberra',
+ 'Australia/Currie',
+ 'Australia/Darwin',
+ 'Australia/Eucla',
+ 'Australia/Hobart',
+ 'Australia/LHI',
+ 'Australia/Lindeman',
+ 'Australia/Lord_Howe',
+ 'Australia/Melbourne',
+ 'Australia/NSW',
+ 'Australia/North',
+ 'Australia/Perth',
+ 'Australia/Queensland',
+ 'Australia/South',
+ 'Australia/Sydney',
+ 'Australia/Tasmania',
+ 'Australia/Victoria',
+ 'Australia/West',
+ 'Australia/Yancowinna',
+ 'Brazil/Acre',
+ 'Brazil/DeNoronha',
+ 'Brazil/East',
+ 'Brazil/West',
+ 'CET',
+ 'CST6CDT',
+ 'Canada/Atlantic',
+ 'Canada/Central',
+ 'Canada/Eastern',
+ 'Canada/Mountain',
+ 'Canada/Newfoundland',
+ 'Canada/Pacific',
+ 'Canada/Saskatchewan',
+ 'Canada/Yukon',
+ 'Chile/Continental',
+ 'Chile/EasterIsland',
+ 'Cuba',
+ 'EET',
+ 'EST',
+ 'EST5EDT',
+ 'Egypt',
+ 'Eire',
+ 'Etc/GMT',
+ 'Etc/GMT+0',
+ 'Etc/GMT+1',
+ 'Etc/GMT+10',
+ 'Etc/GMT+11',
+ 'Etc/GMT+12',
+ 'Etc/GMT+2',
+ 'Etc/GMT+3',
+ 'Etc/GMT+4',
+ 'Etc/GMT+5',
+ 'Etc/GMT+6',
+ 'Etc/GMT+7',
+ 'Etc/GMT+8',
+ 'Etc/GMT+9',
+ 'Etc/GMT-0',
+ 'Etc/GMT-1',
+ 'Etc/GMT-10',
+ 'Etc/GMT-11',
+ 'Etc/GMT-12',
+ 'Etc/GMT-13',
+ 'Etc/GMT-14',
+ 'Etc/GMT-2',
+ 'Etc/GMT-3',
+ 'Etc/GMT-4',
+ 'Etc/GMT-5',
+ 'Etc/GMT-6',
+ 'Etc/GMT-7',
+ 'Etc/GMT-8',
+ 'Etc/GMT-9',
+ 'Etc/GMT0',
+ 'Etc/Greenwich',
+ 'Etc/UCT',
+ 'Etc/UTC',
+ 'Etc/Universal',
+ 'Etc/Zulu',
+ 'Europe/Amsterdam',
+ 'Europe/Andorra',
+ 'Europe/Astrakhan',
+ 'Europe/Athens',
+ 'Europe/Belfast',
+ 'Europe/Belgrade',
+ 'Europe/Berlin',
+ 'Europe/Bratislava',
+ 'Europe/Brussels',
+ 'Europe/Bucharest',
+ 'Europe/Budapest',
+ 'Europe/Busingen',
+ 'Europe/Chisinau',
+ 'Europe/Copenhagen',
+ 'Europe/Dublin',
+ 'Europe/Gibraltar',
+ 'Europe/Guernsey',
+ 'Europe/Helsinki',
+ 'Europe/Isle_of_Man',
+ 'Europe/Istanbul',
+ 'Europe/Jersey',
+ 'Europe/Kaliningrad',
+ 'Europe/Kiev',
+ 'Europe/Kirov',
+ 'Europe/Lisbon',
+ 'Europe/Ljubljana',
+ 'Europe/London',
+ 'Europe/Luxembourg',
+ 'Europe/Madrid',
+ 'Europe/Malta',
+ 'Europe/Mariehamn',
+ 'Europe/Minsk',
+ 'Europe/Monaco',
+ 'Europe/Moscow',
+ 'Europe/Nicosia',
+ 'Europe/Oslo',
+ 'Europe/Paris',
+ 'Europe/Podgorica',
+ 'Europe/Prague',
+ 'Europe/Riga',
+ 'Europe/Rome',
+ 'Europe/Samara',
+ 'Europe/San_Marino',
+ 'Europe/Sarajevo',
+ 'Europe/Saratov',
+ 'Europe/Simferopol',
+ 'Europe/Skopje',
+ 'Europe/Sofia',
+ 'Europe/Stockholm',
+ 'Europe/Tallinn',
+ 'Europe/Tirane',
+ 'Europe/Tiraspol',
+ 'Europe/Ulyanovsk',
+ 'Europe/Uzhgorod',
+ 'Europe/Vaduz',
+ 'Europe/Vatican',
+ 'Europe/Vienna',
+ 'Europe/Vilnius',
+ 'Europe/Volgograd',
+ 'Europe/Warsaw',
+ 'Europe/Zagreb',
+ 'Europe/Zaporozhye',
+ 'Europe/Zurich',
+ 'GB',
+ 'GB-Eire',
+ 'GMT',
+ 'GMT+0',
+ 'GMT-0',
+ 'GMT0',
+ 'Greenwich',
+ 'HST',
+ 'Hongkong',
+ 'Iceland',
+ 'Indian/Antananarivo',
+ 'Indian/Chagos',
+ 'Indian/Christmas',
+ 'Indian/Cocos',
+ 'Indian/Comoro',
+ 'Indian/Kerguelen',
+ 'Indian/Mahe',
+ 'Indian/Maldives',
+ 'Indian/Mauritius',
+ 'Indian/Mayotte',
+ 'Indian/Reunion',
+ 'Iran',
+ 'Israel',
+ 'Jamaica',
+ 'Japan',
+ 'Kwajalein',
+ 'Libya',
+ 'MET',
+ 'MST',
+ 'MST7MDT',
+ 'Mexico/BajaNorte',
+ 'Mexico/BajaSur',
+ 'Mexico/General',
+ 'NZ',
+ 'NZ-CHAT',
+ 'Navajo',
+ 'PRC',
+ 'PST8PDT',
+ 'Pacific/Apia',
+ 'Pacific/Auckland',
+ 'Pacific/Bougainville',
+ 'Pacific/Chatham',
+ 'Pacific/Chuuk',
+ 'Pacific/Easter',
+ 'Pacific/Efate',
+ 'Pacific/Enderbury',
+ 'Pacific/Fakaofo',
+ 'Pacific/Fiji',
+ 'Pacific/Funafuti',
+ 'Pacific/Galapagos',
+ 'Pacific/Gambier',
+ 'Pacific/Guadalcanal',
+ 'Pacific/Guam',
+ 'Pacific/Honolulu',
+ 'Pacific/Johnston',
+ 'Pacific/Kiritimati',
+ 'Pacific/Kosrae',
+ 'Pacific/Kwajalein',
+ 'Pacific/Majuro',
+ 'Pacific/Marquesas',
+ 'Pacific/Midway',
+ 'Pacific/Nauru',
+ 'Pacific/Niue',
+ 'Pacific/Norfolk',
+ 'Pacific/Noumea',
+ 'Pacific/Pago_Pago',
+ 'Pacific/Palau',
+ 'Pacific/Pitcairn',
+ 'Pacific/Pohnpei',
+ 'Pacific/Ponape',
+ 'Pacific/Port_Moresby',
+ 'Pacific/Rarotonga',
+ 'Pacific/Saipan',
+ 'Pacific/Samoa',
+ 'Pacific/Tahiti',
+ 'Pacific/Tarawa',
+ 'Pacific/Tongatapu',
+ 'Pacific/Truk',
+ 'Pacific/Wake',
+ 'Pacific/Wallis',
+ 'Pacific/Yap',
+ 'Poland',
+ 'Portugal',
+ 'ROC',
+ 'ROK',
+ 'Singapore',
+ 'Turkey',
+ 'UCT',
+ 'US/Alaska',
+ 'US/Aleutian',
+ 'US/Arizona',
+ 'US/Central',
+ 'US/East-Indiana',
+ 'US/Eastern',
+ 'US/Hawaii',
+ 'US/Indiana-Starke',
+ 'US/Michigan',
+ 'US/Mountain',
+ 'US/Pacific',
+ 'US/Pacific-New',
+ 'US/Samoa',
+ 'UTC',
+ 'Universal',
+ 'W-SU',
+ 'WET',
+ 'Zulu'
+];
diff --git a/libs/shared/tsconfig.json b/libs/shared/tsconfig.json
index 95cfeb243..95cac5127 100644
--- a/libs/shared/tsconfig.json
+++ b/libs/shared/tsconfig.json
@@ -2,7 +2,6 @@
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
- "esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true
},
diff --git a/libs/types/src/lib/api/admin/awards.ts b/libs/types/src/lib/api/admin/awards.ts
index 7337f8f92..b63ba4cd2 100644
--- a/libs/types/src/lib/api/admin/awards.ts
+++ b/libs/types/src/lib/api/admin/awards.ts
@@ -11,7 +11,7 @@ export const AdminAwardResponseSchema = z.object({
automaticAssignment: z.boolean(),
place: z.number(),
index: z.number(),
- winner: z.string().optional()
+ winner: z.string().nullable().optional()
});
export type Award = z.infer;
diff --git a/libs/types/src/lib/api/lems/graphql/volunteer.graphql b/libs/types/src/lib/api/lems/graphql/volunteer.graphql
index 4eb57af70..8ddaa72a6 100644
--- a/libs/types/src/lib/api/lems/graphql/volunteer.graphql
+++ b/libs/types/src/lib/api/lems/graphql/volunteer.graphql
@@ -4,16 +4,16 @@ A volunteer registered to help run an event
type Volunteer {
"Unique identifier for the volunteer"
id: String!
-
+
"Role the volunteer is assigned (e.g., 'judge', 'referee', 'queuer')"
role: String!
-
+
"Additional context about the volunteer's role assignment"
roleInfo: RoleInfo
-
+
"Optional identifier or badge number for the volunteer"
identifier: String
-
+
"Divisions this volunteer is assigned to"
divisions: [Division!]!
}
diff --git a/libs/types/src/lib/api/portal/divisions.ts b/libs/types/src/lib/api/portal/divisions.ts
index 4ac80c8a6..01390ceee 100644
--- a/libs/types/src/lib/api/portal/divisions.ts
+++ b/libs/types/src/lib/api/portal/divisions.ts
@@ -14,7 +14,7 @@ export const PortalAwardResponseSchema = z.object({
type: z.enum(['PERSONAL', 'TEAM']),
showPlaces: z.boolean(),
place: z.number(),
- winner: z.string().optional()
+ winner: z.string().nullable().optional()
});
const Team = z.object({
diff --git a/libs/types/src/lib/api/portal/events.ts b/libs/types/src/lib/api/portal/events.ts
index adf93d07f..484552895 100644
--- a/libs/types/src/lib/api/portal/events.ts
+++ b/libs/types/src/lib/api/portal/events.ts
@@ -9,7 +9,7 @@ export const PortalEventResponseSchema = z.object({
location: z.string(),
region: z.string(),
timezone: z.string(),
- coordinates: z.string().optional(),
+ coordinates: z.string().nullable().optional(),
seasonId: z.string()
});
diff --git a/libs/types/src/lib/api/scheduler/teams.ts b/libs/types/src/lib/api/scheduler/teams.ts
index 41031fac2..7376af0b0 100644
--- a/libs/types/src/lib/api/scheduler/teams.ts
+++ b/libs/types/src/lib/api/scheduler/teams.ts
@@ -9,4 +9,4 @@ export const SchedulerTeamResponseSchema = z.object({
export type Team = z.infer;
-export const SchedulerTeamsResponseSchema = z.array(SchedulerTeamResponseSchema);
\ No newline at end of file
+export const SchedulerTeamsResponseSchema = z.array(SchedulerTeamResponseSchema);
diff --git a/package-lock.json b/package-lock.json
index 5c466ae32..c013f3fe5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -103,12 +103,14 @@
"@swc/plugin-emotion": "^14.14.1",
"@types/archiver": "^8.0.0",
"@types/bcrypt": "^6.0.0",
+ "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1",
"@types/grecaptcha": "^3.0.9",
"@types/ioredis": "^4.28.10",
"@types/jsonwebtoken": "^9.0.10",
+ "@types/morgan": "^1.9.10",
"@types/node": "^24.13.2",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.17",
@@ -132,7 +134,7 @@
"run-script-os": "^1.1.6",
"ts-node": "10.9.2",
"tsx": "^4.22.4",
- "typescript": "^5.9.3",
+ "typescript": "^6.0.0",
"typescript-eslint": "^8.62.1",
"webpack": "5.108.3",
"webpack-cli": "^7.0.0",
@@ -4234,9 +4236,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4253,9 +4252,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4272,9 +4268,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4291,9 +4284,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4310,9 +4300,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4329,9 +4316,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4348,9 +4332,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4367,9 +4348,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -4386,9 +4364,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4411,9 +4386,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4436,9 +4408,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4461,9 +4430,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4486,9 +4452,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4511,9 +4474,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4536,9 +4496,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -4561,9 +4518,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -6618,9 +6572,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -6637,9 +6588,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -6656,9 +6604,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -6675,9 +6620,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -6909,6 +6851,20 @@
"node": ">=10"
}
},
+ "node_modules/@nx/eslint/node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/@nx/express": {
"version": "23.0.1",
"resolved": "https://registry.npmjs.org/@nx/express/-/express-23.0.1.tgz",
@@ -7656,9 +7612,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -7673,9 +7626,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -7690,9 +7640,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -7707,9 +7654,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10039,9 +9983,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10060,9 +10001,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10081,9 +10019,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10102,9 +10037,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10123,9 +10055,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10144,9 +10073,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10586,9 +10512,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10603,9 +10526,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10620,9 +10540,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -10637,9 +10554,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -11490,9 +11404,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11509,9 +11420,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11528,9 +11436,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11547,9 +11452,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11566,9 +11468,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11585,9 +11484,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@@ -11888,6 +11784,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
+ "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/express": "*"
+ }
+ },
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
@@ -12188,6 +12094,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/morgan": {
+ "version": "1.9.10",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz",
+ "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -23034,9 +22950,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -23059,9 +22972,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -23084,9 +22994,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -23109,9 +23016,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -25024,9 +24928,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25043,9 +24944,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25062,9 +24960,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25081,9 +24976,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25100,9 +24992,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25119,9 +25008,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25138,9 +25024,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25157,9 +25040,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -25176,9 +25056,6 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25201,9 +25078,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25226,9 +25100,6 @@
"cpu": [
"ppc64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25251,9 +25122,6 @@
"cpu": [
"riscv64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25276,9 +25144,6 @@
"cpu": [
"s390x"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25301,9 +25166,6 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25326,9 +25188,6 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -25351,9 +25210,6 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -29909,7 +29765,6 @@
"arm"
],
"dev": true,
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -29927,7 +29782,6 @@
"arm64"
],
"dev": true,
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -29945,7 +29799,6 @@
"arm"
],
"dev": true,
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -29963,7 +29816,6 @@
"arm64"
],
"dev": true,
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -29981,7 +29833,6 @@
"riscv64"
],
"dev": true,
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -29999,7 +29850,6 @@
"x64"
],
"dev": true,
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -30017,7 +29867,6 @@
"riscv64"
],
"dev": true,
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -30035,7 +29884,6 @@
"x64"
],
"dev": true,
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -32577,9 +32425,9 @@
"license": "MIT"
},
"node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
diff --git a/package.json b/package.json
index f49104c81..d5dc5b508 100644
--- a/package.json
+++ b/package.json
@@ -109,12 +109,14 @@
"@swc/plugin-emotion": "^14.14.1",
"@types/archiver": "^8.0.0",
"@types/bcrypt": "^6.0.0",
+ "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/express-fileupload": "^1.5.1",
"@types/grecaptcha": "^3.0.9",
"@types/ioredis": "^4.28.10",
"@types/jsonwebtoken": "^9.0.10",
+ "@types/morgan": "^1.9.10",
"@types/node": "^24.13.2",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.17",
@@ -138,7 +140,7 @@
"run-script-os": "^1.1.6",
"ts-node": "10.9.2",
"tsx": "^4.22.4",
- "typescript": "^5.9.3",
+ "typescript": "^6.0.0",
"typescript-eslint": "^8.62.1",
"webpack": "5.108.3",
"webpack-cli": "^7.0.0",
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 8fb88bdca..f6ddd2e25 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -11,17 +11,18 @@
"target": "es2024",
"module": "es2022",
"lib": ["ES2024", "dom"],
+ "strict": true,
+ "types": ["node"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
- "baseUrl": ".",
"paths": {
- "@lems/database": ["libs/database/src/index.ts"],
- "@lems/localization": ["libs/localization/src/index.ts"],
- "@lems/presentations": ["libs/presentations/src/index.ts"],
- "@lems/shared": ["libs/shared/src/index.ts"],
- "@lems/shared/*": ["libs/shared/src/lib/*"],
- "@lems/types": ["libs/types/src/index.ts"],
- "@lems/types/*": ["libs/types/src/lib/*"],
+ "@lems/database": ["./libs/database/src/index.ts"],
+ "@lems/localization": ["./libs/localization/src/index.ts"],
+ "@lems/presentations": ["./libs/presentations/src/index.ts"],
+ "@lems/shared": ["./libs/shared/src/index.ts"],
+ "@lems/shared/*": ["./libs/shared/src/lib/*"],
+ "@lems/types": ["./libs/types/src/index.ts"],
+ "@lems/types/*": ["./libs/types/src/lib/*"]
}
},
"exclude": ["node_modules", "tmp"]