i18n
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Failing after 10s
CI/CD Pipeline / Build and Push App Docker Image (push) Skipped
CI/CD Pipeline / Deploy to Kubernetes (push) Skipped

This commit is contained in:
2026-08-08 15:29:40 -07:00
parent 22c282d8fa
commit 0a7b5fa00f
178 changed files with 13099 additions and 1285 deletions
@@ -7,6 +7,7 @@ import {
} from "@mantine/core";
import { TrashIcon } from "@phosphor-icons/react";
import { useCallback, memo } from "react";
import { Trans, Plural, useLingui } from "@lingui/react/macro";
import { useFreeAgents } from "../queries";
import PlayerAvatar from "@/components/player-avatar";
import { PlayerInfo, Player } from "@/features/players/types";
@@ -51,6 +52,7 @@ const PlayerItem = memo(({ player, onRemove, disabled }: PlayerItemProps) => {
});
const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
const { t } = useLingui();
const { data: freeAgents = [], isLoading } = useFreeAgents(tournamentId);
const { data: allPlayers = [] } = usePlayers();
@@ -112,10 +114,10 @@ const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
<Stack gap="lg" w="100%">
<Stack gap="xs">
<Text fw={600} size="sm">
Add Player
<Trans>Add Player</Trans>
</Text>
<Typeahead
placeholder="Search for players to enroll..."
placeholder={t`Search for players to enroll...`}
onSelect={handleEnrollPlayer}
searchFn={searchPlayers}
renderOption={renderPlayerOption}
@@ -127,10 +129,10 @@ const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
<Stack gap="xs">
<Group justify="space-between">
<Text fw={600} size="sm">
Enrolled Players
<Trans>Enrolled Players</Trans>
</Text>
<Text size="xs" c="dimmed">
{enrolledPlayers.length} players
<Plural value={enrolledPlayers.length} one="# player" other="# players" />
</Text>
</Group>
@@ -140,7 +142,7 @@ const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
</Group>
) : !hasEnrolledPlayers ? (
<Text size="sm" c="dimmed" ta="center" py="lg">
No players enrolled yet
<Trans>No players enrolled yet</Trans>
</Text>
) : (
<Stack gap="xs" w="100%">
@@ -8,6 +8,7 @@ import {
import Typeahead, { TypeaheadOption } from "@/components/typeahead";
import { TrashIcon } from "@phosphor-icons/react";
import { useState, useCallback, useMemo, memo } from "react";
import { Trans, Plural, useLingui } from "@lingui/react/macro";
import { useTournament, useUnenrolledTeams } from "../queries";
import useEnrollTeam from "../hooks/use-enroll-team";
import useUnenrollTeam from "../hooks/use-unenroll-team";
@@ -65,6 +66,7 @@ const TeamItem = memo(({ team, onUnenroll, disabled, isRegional }: TeamItemProps
});
const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
const { t } = useLingui();
const { data: tournament, isLoading: tournamentLoading } =
useTournament(tournamentId);
const { data: unenrolledTeams = [], isLoading: unenrolledLoading } =
@@ -131,10 +133,10 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
<Stack gap="lg" w="100%">
<Stack gap="xs">
<Text fw={600} size="sm">
Add Team
<Trans>Add Team</Trans>
</Text>
<Typeahead
placeholder="Search for teams to enroll..."
placeholder={t`Search for teams to enroll...`}
onSelect={handleEnrollTeam}
searchFn={searchTeams}
renderOption={renderTeamOption}
@@ -146,10 +148,10 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
<Stack gap="xs">
<Group justify="space-between">
<Text fw={600} size="sm">
Enrolled Teams
<Trans>Enrolled Teams</Trans>
</Text>
<Text size="xs" c="dimmed">
{enrolledTeams.length} teams
<Plural value={enrolledTeams.length} one="# team" other="# teams" />
</Text>
</Group>
@@ -159,7 +161,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
</Group>
) : !hasEnrolledTeams ? (
<Text size="sm" c="dimmed" ta="center" py="lg">
No teams enrolled yet
<Trans>No teams enrolled yet</Trans>
</Text>
) : (
<Stack gap="xs" w="100%">
@@ -6,6 +6,7 @@ import { useState, useCallback } from "react";
import { useTournament } from "../queries";
import useUpdateTournament from "../hooks/use-update-tournament";
import { RichTextEditor } from "@/components/rich-text-editor";
import { Trans } from "@lingui/react/macro";
interface EditRulesProps {
tournamentId: string;
@@ -35,8 +36,8 @@ const EditRules = ({ tournamentId, onClose }: EditRulesProps) => {
return (
<Stack gap="xs" w="100%">
<RichTextEditor value={value || ""} onChange={setValue} />
<Button onClick={() => handleSubmit(value)}>Submit</Button>
<Button variant="subtle" color="red" onClick={onClose}>Cancel</Button>
<Button onClick={() => handleSubmit(value)}><Trans>Submit</Trans></Button>
<Button variant="subtle" color="red" onClick={onClose}><Trans>Cancel</Trans></Button>
</Stack>
);
};
@@ -11,6 +11,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { MatchForm } from "@/features/bracket/components/match-form";
import TeamAvatar from "@/components/team-avatar";
import AnimatedScore from "@/features/matches/components/animated-score";
import { useLingui } from "@lingui/react/macro";
interface GroupMatchCardProps {
match: Match;
@@ -19,6 +20,7 @@ interface GroupMatchCardProps {
}
const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, nextUpMatchId }) => {
const { t } = useLingui();
const queryClient = useQueryClient();
const editSheet = useSheet();
@@ -32,7 +34,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
const start = useServerMutation({
mutationFn: startMatch,
successMessage: "Match started!",
successMessage: t`Match started!`,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: tournamentKeys.details(match.tournament.id),
@@ -113,7 +115,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
style={{ flex: 1, minWidth: 0 }}
lineClamp={1}
>
{match.home?.name || "TBD"}
{match.home?.name || t`TBD`}
</Text>
</Group>
{isEnded && match.home_cups !== undefined && (
@@ -143,7 +145,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
style={{ flex: 1, minWidth: 0 }}
lineClamp={1}
>
{match.away?.name || "TBD"}
{match.away?.name || t`TBD`}
</Text>
</Group>
{isEnded && match.away_cups !== undefined && (
@@ -163,7 +165,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
{showStartButton && (
<ActionIcon
color="green"
aria-label="Start match"
aria-label={t`Start match`}
onClick={handleStartMatch}
loading={start.isPending}
size="md"
@@ -182,7 +184,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
{showEditButton && (
<ActionIcon
color="blue"
aria-label="Edit match score"
aria-label={t`Edit match score`}
onClick={editSheet.open}
size="md"
h="100%"
@@ -199,7 +201,7 @@ const GroupMatchCard: React.FC<GroupMatchCardProps> = ({ match, showControls, ne
</Flex>
{showControls && (
<Sheet title="End Match" opened={editSheet.isOpen} onChange={editSheet.toggle}>
<Sheet title={t`End Match`} opened={editSheet.isOpen} onChange={editSheet.toggle}>
<MatchForm
match={match}
onSubmit={handleFormSubmit}
@@ -1,6 +1,7 @@
import { Stack, Text, Card, Group, Box } from "@mantine/core";
import { TeamInfo } from "@/features/teams/types";
import TeamAvatar from "@/components/team-avatar";
import { Trans, Plural } from "@lingui/react/macro";
interface GroupAssignment {
groupIndex: number;
@@ -20,10 +21,10 @@ const GroupPreview: React.FC<GroupPreviewProps> = ({ groups }) => {
<Stack gap="sm">
<Group gap="xs" align="center">
<Text fw={600} size="sm">
Group {group.groupName}
<Trans>Group {group.groupName}</Trans>
</Text>
<Text size="xs" c="dimmed">
({group.teams.length} teams)
(<Plural value={group.teams.length} one="# team" other="# teams" />)
</Text>
</Group>
@@ -10,6 +10,7 @@ import { populateKnockoutBracket } from "@/features/matches/server";
import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "../queries";
import { computeGroupStageOrder } from "../utils/match-queue";
import { Trans, useLingui } from "@lingui/react/macro";
interface GroupStageViewProps {
groups: Group[];
@@ -43,12 +44,13 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
groupConfig,
nextUpMatchId,
}) => {
const { t } = useLingui();
const queryClient = useQueryClient();
const [expandedTeams, setExpandedTeams] = useState<Record<string, boolean>>({});
const populateKnockoutMutation = useServerMutation({
mutationFn: populateKnockoutBracket,
successMessage: "Knockout bracket populated successfully!",
successMessage: t`Knockout bracket populated successfully!`,
onSuccess: () => {
if (tournamentId) {
queryClient.invalidateQueries({ queryKey: tournamentKeys.details(tournamentId) });
@@ -289,7 +291,7 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
return (
<Box p="md">
<Text size="sm" c="dimmed" ta="center">
No groups have been created yet
<Trans>No groups have been created yet</Trans>
</Text>
</Box>
);
@@ -301,15 +303,15 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
<Box p="md">
<Stack gap="md">
{showGenerateKnockoutButton && (
<Alert color="blue" title="Group Stage Complete" icon={<CaretCircleUpIcon size={20} />}>
<Alert color="blue" title={t`Group Stage Complete`} icon={<CaretCircleUpIcon size={20} />}>
<Stack gap="xs">
<Text size="sm">All group matches are finished! Populate the knockout bracket to advance qualified teams.</Text>
<Text size="sm"><Trans>All group matches are finished! Populate the knockout bracket to advance qualified teams.</Trans></Text>
<Button
onClick={handlePopulateKnockout}
loading={populateKnockoutMutation.isPending}
size="sm"
>
Populate Knockout Bracket
<Trans>Populate Knockout Bracket</Trans>
</Button>
</Stack>
</Alert>
@@ -330,7 +332,7 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
}}
>
<MantineGroup gap="xs" justify="center" wrap="nowrap">
<Text fw={600} size="sm">Group {group.name}</Text>
<Text fw={600} size="sm"><Trans>Group {group.name}</Trans></Text>
<Text size="xs" c="dimmed" style={{ opacity: 0.7 }}>
({completedMatches}/{totalMatches})
</Text>
@@ -358,12 +360,12 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
onClick={() => toggleTeams(group.id)}
>
<Text fw={600} size="sm">
Standings ({standings.length})
<Trans>Standings ({standings.length})</Trans>
</Text>
<ActionIcon
variant="subtle"
size="sm"
aria-label={expandedTeams[group.id] ? "Collapse standings" : "Expand standings"}
aria-label={expandedTeams[group.id] ? t`Collapse standings` : t`Expand standings`}
>
{expandedTeams[group.id] ? <CaretCircleUpIcon size={16} /> : <CaretCircleDownIcon size={16} />}
</ActionIcon>
@@ -402,12 +404,12 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
<MantineGroup gap="xs" wrap="nowrap">
{isWildcard && (
<Badge size="xs" color="yellow" variant="light">
WC
<Trans>WC</Trans>
</Badge>
)}
{isQualified && (
<Badge size="xs" variant="light">
Q
<Trans>Q</Trans>
</Badge>
)}
<Text size="xs" c="dimmed" fw={500} miw={35} ta="center">
@@ -431,7 +433,7 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
})
) : (
<Text size="sm" c="dimmed" ta="center" py="md">
No teams assigned
<Trans>No teams assigned</Trans>
</Text>
)}
</Stack>
@@ -441,7 +443,7 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
{groupMatches.length === 0 ? (
<Card withBorder radius="md" p="xl">
<Text size="sm" c="dimmed" ta="center">
No matches scheduled
<Trans>No matches scheduled</Trans>
</Text>
</Card>
) : (
@@ -17,12 +17,14 @@ import EditEnrolledPlayers from "./edit-enrolled-players";
import ListLink from "@/components/list-link";
import React from "react";
import EditRules from "./edit-rules";
import { useLingui } from "@lingui/react/macro";
interface ManageTournamentProps {
tournamentId: string;
}
const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
const { t } = useLingui();
const { data: tournament } = useTournament(tournamentId);
if (!tournament) throw new Error("Tournament not found.");
@@ -48,43 +50,43 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
<>
<List p="0">
<ListButton
label="Edit Tournament"
label={t`Edit Tournament`}
Icon={HardDrivesIcon}
onClick={openEditTournament}
/>
<ListButton
label="Edit Rules"
label={t`Edit Rules`}
Icon={PencilLineIcon}
onClick={openEditRules}
/>
<ListButton
label="Edit Enrollments"
label={t`Edit Enrollments`}
Icon={UsersThreeIcon}
onClick={openEditTeams}
/>
{tournament.regional && (
<ListLink
label="Assign Partners"
label={t`Assign Partners`}
Icon={ShuffleIcon}
to={`/admin/tournaments/${tournamentId}/assign-partners`}
/>
)}
{!tournament.regional && (
<ListLink
label="Manage Team Songs/Logos"
label={t`Manage Team Songs/Logos`}
Icon={UsersIcon}
to={`/admin/tournaments/${tournamentId}/teams`}
/>
)}
<ListLink
label="Run Tournament"
label={t`Run Tournament`}
Icon={TreeStructureIcon}
to={`/admin/tournaments/run/${tournamentId}`}
/>
</List>
<Sheet
title="Edit Tournament"
title={t`Edit Tournament`}
opened={editTournamentOpened}
onChange={closeEditTournament}
>
@@ -104,7 +106,7 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
</Sheet>
<Sheet
title="Edit Rules"
title={t`Edit Rules`}
opened={editRulesOpened}
onChange={closeEditRules}
>
@@ -112,7 +114,7 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
</Sheet>
<Sheet
title={tournament.regional === true ? "Manage Enrollments" : "Edit Enrolled Teams"}
title={tournament.regional === true ? t`Manage Enrollments` : t`Edit Enrolled Teams`}
opened={editTeamsOpened}
onChange={closeEditTeams}
>
@@ -2,6 +2,9 @@ import { Stack, Group, Text, ThemeIcon } from "@mantine/core";
import { CrownIcon, MedalIcon } from "@phosphor-icons/react";
import { TeamInfo } from "@/features/teams/types";
import { Tournament } from "../types";
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react/macro";
import type { MessageDescriptor } from "@lingui/core";
interface PodiumProps {
tournament: Tournament;
@@ -40,7 +43,7 @@ const tierConfig: Record<
PodiumTier,
{
color: string;
label: string;
label: MessageDescriptor;
labelColor: string;
icon: React.ReactNode;
iconSize: "xl" | "lg";
@@ -50,7 +53,7 @@ const tierConfig: Record<
> = {
first: {
color: "yellow",
label: "Champions",
label: msg`Champions`,
labelColor:
"light-dark(var(--mantine-color-yellow-8), var(--mantine-color-yellow-4))",
icon: <CrownIcon size={24} />,
@@ -60,7 +63,7 @@ const tierConfig: Record<
},
second: {
color: "gray",
label: "2nd place",
label: msg`2nd place`,
labelColor:
"light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4))",
icon: <MedalIcon size={20} />,
@@ -70,7 +73,7 @@ const tierConfig: Record<
},
third: {
color: "orange",
label: "3rd place",
label: msg`3rd place`,
labelColor:
"light-dark(var(--mantine-color-orange-8), var(--mantine-color-orange-4))",
icon: <MedalIcon size={18} />,
@@ -81,6 +84,7 @@ const tierConfig: Record<
};
const PodiumRow = ({ team, tier }: { team: TeamInfo; tier: PodiumTier }) => {
const { t } = useLingui();
const config = tierConfig[tier];
return (
@@ -121,7 +125,7 @@ const PodiumRow = ({ team, tier }: { team: TeamInfo; tier: PodiumTier }) => {
mt={4}
mr={4}
>
{config.label}
{t(config.label)}
</Text>
</Group>
);
@@ -6,31 +6,36 @@ import SwipeableTabs from "@/components/swipeable-tabs";
import { useTournament } from "../../queries";
import MatchList from "@/features/matches/components/match-list";
import { TournamentStats } from "../tournament-stats";
import { useLingui } from "@lingui/react/macro";
interface ProfileProps {
id: string;
}
const Profile = ({ id }: ProfileProps) => {
const { t } = useLingui();
const { data: tournament } = useTournament(id);
if (!tournament) return null;
const tabs = useMemo(() => [
{
label: "Overview",
label: t`Overview`,
value: "overview",
content: <TournamentStats tournament={tournament} />
},
{
label: "Matches",
label: t`Matches`,
value: "matches",
content: <MatchList matches={tournament.matches?.sort((a, b) => b.order - a.order) || []} />
},
{
label: "Teams",
label: t`Teams`,
value: "teams",
content: <>
<TeamList teams={tournament.teams || []} isRegional={tournament.regional} />
</>
}
], [tournament]);
], [tournament, t]);
return <>
<Header tournament={tournament} />
@@ -2,6 +2,7 @@ import { Box, Divider, Group, Paper, Skeleton, Stack } from "@mantine/core";
import SwipeableTabs from "@/components/swipeable-tabs";
import TeamList from "@/features/teams/components/team-list";
import HeaderSkeleton from "./header-skeleton";
import { useLingui } from "@lingui/react/macro";
const MatchCardSkeleton = ({ opacity = 1 }: { opacity?: number }) => (
<Paper px="md" py="md" withBorder radius="md" style={{ opacity }}>
@@ -72,17 +73,21 @@ const OverviewSkeleton = () => (
);
const ProfileSkeleton = () => {
const { t } = useLingui();
const tabs = [
{
label: "Overview",
label: t`Overview`,
value: "overview",
content: <OverviewSkeleton />,
},
{
label: "Matches",
label: t`Matches`,
value: "matches",
content: <MatchListSkeleton />,
},
{
label: "Teams",
label: t`Teams`,
value: "teams",
content: <TeamList teams={[]} loading />,
},
];
@@ -19,6 +19,7 @@ import { BracketData } from "@/features/bracket/types";
import BracketView from "@/features/bracket/components/bracket-view";
import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "../queries";
import { Trans, Plural, useLingui } from "@lingui/react/macro";
interface SeedTournamentProps {
tournamentId: string;
@@ -31,6 +32,7 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
teams,
isRegional,
}) => {
const { t } = useLingui();
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
const { data: bracketPreview } = useBracketPreview(teams.length);
const queryClient = useQueryClient();
@@ -57,7 +59,7 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
const generateBracket = useServerMutation({
mutationFn: generateTournamentBracket,
successMessage: "Tournament bracket generated successfully!",
successMessage: t`Tournament bracket generated successfully!`,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: tournamentKeys.details(tournamentId),
@@ -108,10 +110,14 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
<Group gap="xs" p="md" pb="sm" align="center">
<Text fw={600} size="lg">
Team Seeding
<Trans>Team Seeding</Trans>
</Text>
<Text size="sm" c="dimmed" ml="auto">
{orderedTeams.length} teams
<Plural
value={orderedTeams.length}
one="# team"
other="# teams"
/>
</Text>
</Group>
@@ -122,7 +128,7 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
loading={generateBracket.isPending}
disabled={orderedTeams.length === 0}
>
Confirm Seeding
<Trans>Confirm Seeding</Trans>
</Button>
<DragDropContext onDragEnd={handleDragEnd}>
@@ -201,7 +207,7 @@ const SeedTournament: React.FC<SeedTournamentProps> = ({
loading={generateBracket.isPending}
disabled={orderedTeams.length === 0}
>
Confirm Seeding
<Trans>Confirm Seeding</Trans>
</Button>
</Stack>
@@ -12,6 +12,8 @@ import {
Box,
} from "@mantine/core";
import { InfoIcon } from "@phosphor-icons/react";
import { Trans, Plural, useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
import { generateGroupStage } from "../server";
import { TeamInfo } from "@/features/teams/types";
@@ -21,6 +23,10 @@ import {
getGroupName,
GroupConfigOption,
} from "../utils/group-config";
import {
formatGroupRankLabel,
formatWildcardLabel,
} from "@/features/bracket/utils/group-label";
import GroupPreview from "./group-preview";
import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "../queries";
@@ -39,17 +45,18 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
teams,
}) => {
const queryClient = useQueryClient();
const { t, i18n } = useLingui();
const [selectedConfigIndex, setSelectedConfigIndex] = useState<string>("0");
const [seed, setSeed] = useState<number>(Date.now());
const configurations = useMemo(() => {
try {
return calculateGroupConfigurations(teams.length);
return calculateGroupConfigurations(i18n, teams.length);
} catch (error) {
return [];
}
}, [teams.length]);
}, [teams.length, i18n]);
const selectedConfig: GroupConfigOption | null = useMemo(() => {
const index = parseInt(selectedConfigIndex);
@@ -105,20 +112,20 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
const group1 = i % selectedConfig.num_groups;
const rankIndex1 = Math.floor(i / selectedConfig.num_groups);
const rank1 = rankIndex1 + 1;
const groupName1 = groupNames[group1] || `Group ${group1 + 1}`;
const rankSuffix1 = rank1 === 1 ? '1st' : rank1 === 2 ? '2nd' : rank1 === 3 ? '3rd' : `${rank1}th`;
seedLabels[seedIndex++] = `${groupName1} ${rankSuffix1}`;
const groupName1 = groupNames[group1] ||
i18n._({ ...msg`Group {n}`, values: { n: group1 + 1 } });
seedLabels[seedIndex++] = formatGroupRankLabel(i18n, groupName1, rank1);
const group2 = (i + 1) % selectedConfig.num_groups;
const rankIndex2 = selectedConfig.advance_per_group - 1 - rankIndex1;
const rank2 = rankIndex2 + 1;
const groupName2 = groupNames[group2] || `Group ${group2 + 1}`;
const rankSuffix2 = rank2 === 1 ? '1st' : rank2 === 2 ? '2nd' : rank2 === 3 ? '3rd' : `${rank2}th`;
seedLabels[seedIndex++] = `${groupName2} ${rankSuffix2}`;
const groupName2 = groupNames[group2] ||
i18n._({ ...msg`Group {n}`, values: { n: group2 + 1 } });
seedLabels[seedIndex++] = formatGroupRankLabel(i18n, groupName2, rank2);
}
for (let i = 0; i < wildcardsNeeded; i++) {
seedLabels[seedIndex++] = `Wildcard ${i + 1}`;
seedLabels[seedIndex++] = formatWildcardLabel(i18n, i + 1);
}
const ordersMap: Record<number, number> = {};
@@ -154,11 +161,11 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
);
return { matches: placeholderMatches, orders: ordersMap };
}, [knockoutTeamCount, selectedConfig]);
}, [knockoutTeamCount, selectedConfig, i18n]);
const generateGroups = useServerMutation({
mutationFn: generateGroupStage,
successMessage: "Group stage generated successfully!",
successMessage: t`Group stage generated successfully!`,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: tournamentKeys.details(tournamentId),
@@ -195,8 +202,10 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
if (configurations.length === 0) {
return (
<Alert color="red" title="Cannot create groups" icon={<InfoIcon />}>
Need at least 4 teams to create a group stage format. Current team count: {teams.length}
<Alert color="red" title={t`Cannot create groups`} icon={<InfoIcon />}>
<Trans>
Need at least 4 teams to create a group stage format. Current team count: {teams.length}
</Trans>
</Alert>
);
}
@@ -209,17 +218,17 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
<Group gap="xs" p="md" pb="sm" align="center">
<Text fw={600} size="lg">
Group Stage Setup
<Trans>Group Stage Setup</Trans>
</Text>
<Text size="sm" c="dimmed" ml="auto">
{teams.length} teams
<Plural value={teams.length} one="# team" other="# teams" />
</Text>
</Group>
<Stack gap="md" p="md" pt={0}>
<Stack gap="xs">
<Text size="sm" fw={500}>
Group Configuration
<Trans>Group Configuration</Trans>
</Text>
<Select
value={selectedConfigIndex}
@@ -237,18 +246,27 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
{selectedConfig && (
<Stack gap="xs">
<Text size="xs" c="dimmed">
{selectedConfig.total_group_matches} total group stage matches
<Plural
value={selectedConfig.total_group_matches}
one="# total group stage match"
other="# total group stage matches"
/>
</Text>
{selectedConfig.wildcards_needed > 0 && (
<Text size="xs" c="yellow">
⚠ {selectedConfig.wildcards_needed} wildcard spot{selectedConfig.wildcards_needed > 1 ? 's' : ''} needed for knockout bracket
⚠{" "}
<Plural
value={selectedConfig.wildcards_needed}
one="# wildcard spot needed for knockout bracket"
other="# wildcard spots needed for knockout bracket"
/>
</Text>
)}
</Stack>
)}
<Button size="sm" variant="light" onClick={handleShuffle}>
Shuffle Groups
<Trans>Shuffle Groups</Trans>
</Button>
<Button
@@ -257,7 +275,7 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
loading={generateGroups.isPending}
disabled={!selectedConfig}
>
Confirm Setup
<Trans>Confirm Setup</Trans>
</Button>
</Stack>
</Stack>
@@ -273,11 +291,32 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
<Box>
<Divider mb="lg" />
<Title order={3} ta="center" mb="md">
Knockout Bracket Preview ({selectedConfig?.knockout_size} Teams)
<Trans>
Knockout Bracket Preview ({selectedConfig?.knockout_size} Teams)
</Trans>
</Title>
<Text size="sm" c="dimmed" ta="center" mb="lg">
Top {selectedConfig?.advance_per_group} team{selectedConfig?.advance_per_group !== 1 ? 's' : ''} from each group will advance
{selectedConfig?.wildcards_needed ? ` + ${selectedConfig.wildcards_needed} wildcard${selectedConfig.wildcards_needed > 1 ? 's' : ''}` : ''}
<Trans>
Top{" "}
<Plural
value={selectedConfig?.advance_per_group ?? 0}
one="# team"
other="# teams"
/>{" "}
from each group will advance
{selectedConfig?.wildcards_needed ? (
<>
{" + "}
<Plural
value={selectedConfig.wildcards_needed}
one="# wildcard"
other="# wildcards"
/>
</>
) : (
""
)}
</Trans>
</Text>
<Box
style={{
@@ -2,9 +2,11 @@ import { Group, Stack, ThemeIcon, Text } from "@mantine/core";
import { Tournament } from "../../types";
import { CalendarIcon, MapPinIcon, TrophyIcon } from "@phosphor-icons/react";
import { useMemo } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import GlitchAvatar from "@/components/glitch-avatar";
const Header = ({ tournament }: { tournament: Tournament }) => {
const { i18n } = useLingui();
const tournamentStart = useMemo(
() => new Date(tournament.start_time),
[tournament.start_time]
@@ -46,16 +48,18 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
<CalendarIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed" lineClamp={1}>
{tournamentStart.toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
})}{" "}
at{" "}
{tournamentStart.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
<Trans>
{tournamentStart.toLocaleDateString(i18n.locale, {
weekday: "short",
month: "short",
day: "numeric",
})}{" "}
at{" "}
{tournamentStart.toLocaleTimeString(i18n.locale, {
hour: "2-digit",
minute: "2-digit",
})}
</Trans>
</Text>
</Group>
</Stack>
@@ -4,6 +4,7 @@ import { useAuth } from "@/contexts/auth-context";
import { Box, Card, Center, Divider, Stack, Text } from "@mantine/core";
import { ClockIcon } from "@phosphor-icons/react";
import { isPredictionLocked, isTournamentPredictable } from "@/features/predictions/utils";
import { Trans, useLingui } from "@lingui/react/macro";
import { predictionQueries } from "@/features/predictions/queries";
import { useServerQuery } from "@/lib/tanstack-query/hooks";
import { Match } from "@/features/matches/types";
@@ -22,6 +23,7 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({
tournament,
}) => {
const { user, roles } = useAuth();
const { t } = useLingui();
const isAdmin = useMemo(() => roles.includes("Admin"), [roles]);
@@ -109,21 +111,21 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({
{isTournamentOver && (
<Box>
<SectionHeading label="Final Results" />
<SectionHeading label={t`Final Results`} />
<Podium tournament={tournament} />
</Box>
)}
{liveMatches.length > 0 && (
<Box>
<SectionHeading label="Live Matches" live count={liveMatches.length} />
<SectionHeading label={t`Live Matches`} live count={liveMatches.length} />
<MatchCarousel matches={liveMatches} />
</Box>
)}
{upcomingMatches.length > 0 && (
<Box>
<SectionHeading label="Upcoming" count={upcomingMatches.length} />
<SectionHeading label={t`Upcoming`} count={upcomingMatches.length} />
{/* SIDE BETS SLOT — see docs/side-bets.md */}
<MatchCarousel matches={upcomingMatches} />
</Box>
@@ -137,7 +139,7 @@ const StartedTournament: React.FC<{ tournament: Tournament }> = ({
<Stack align="center" gap="md">
<ClockIcon size={48} color="var(--mantine-color-dimmed)" />
<Text size="lg" fw={500} c="dimmed">
No active matches
<Trans>No active matches</Trans>
</Text>
</Stack>
</Center>
@@ -17,6 +17,8 @@ import {
UsersIcon,
} from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { Trans, useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
import Sheet from "@/components/sheet/sheet";
import { useSheet } from "@/hooks/use-sheet";
import TeamList from "@/features/teams/components/team-list";
@@ -73,13 +75,14 @@ const NavGrid = ({
const navigate = useNavigate();
const teamsSheet = useSheet();
const rulesSheet = useSheet();
const { t, i18n } = useLingui();
return (
<Box px="md">
<SimpleGrid cols={3} spacing="sm" verticalSpacing="sm">
{isAdmin && (
<NavTile
label="Manage"
label={t`Manage`}
Icon={UsersIcon}
onClick={() =>
navigate({ to: `/admin/tournaments/${tournament.id}` })
@@ -88,7 +91,7 @@ const NavGrid = ({
)}
{hasGroupStage && (
<NavTile
label="Groups"
label={t`Groups`}
Icon={ListDashes}
onClick={() =>
navigate({ to: `/tournaments/${tournament.id}/groups` })
@@ -96,7 +99,7 @@ const NavGrid = ({
/>
)}
<NavTile
label="Bracket"
label={t`Bracket`}
Icon={TreeStructureIcon}
onClick={() =>
navigate({ to: `/tournaments/${tournament.id}/bracket` })
@@ -104,7 +107,7 @@ const NavGrid = ({
/>
{isPredictable && !predictionsLocked && (
<NavTile
label={hasSubmitted ? "Edit Prediction" : "Make Prediction"}
label={hasSubmitted ? t`Edit Prediction` : t`Make Prediction`}
Icon={WizardOrbIcon}
onClick={() =>
navigate({ to: `/tournaments/${tournament.id}/predictions/make` })
@@ -113,7 +116,7 @@ const NavGrid = ({
)}
{isPredictable && (predictionsLocked || hasSubmitted) && (
<NavTile
label="Predictions"
label={t`Predictions`}
Icon={WizardOrbIcon}
onClick={() =>
navigate({ to: `/tournaments/${tournament.id}/predictions` })
@@ -121,23 +124,28 @@ const NavGrid = ({
/>
)}
<NavTile
label={`Teams${
tournament.teams ? ` (${tournament.teams.length})` : ""
}`}
label={
tournament.teams
? i18n._({
...msg`{count, plural, one {Teams (#)} other {Teams (#)}}`,
values: { count: tournament.teams.length },
})
: t`Teams`
}
Icon={UsersIcon}
onClick={teamsSheet.open}
/>
<NavTile label="Rules" Icon={ListIcon} onClick={rulesSheet.open} />
<NavTile label={t`Rules`} Icon={ListIcon} onClick={rulesSheet.open} />
</SimpleGrid>
<Sheet title="Enrolled Teams" {...teamsSheet.props}>
<Sheet title={t`Enrolled Teams`} {...teamsSheet.props}>
<TeamList
teams={tournament.teams || []}
isRegional={tournament.regional}
/>
</Sheet>
<Sheet title="Tournament Rules" {...rulesSheet.props}>
<Sheet title={t`Tournament Rules`} {...rulesSheet.props}>
<Stack gap="xs">
<Suspense
fallback={
@@ -154,7 +162,7 @@ const NavGrid = ({
<RulesContent content={tournament.rules || ""} />
</Suspense>
<Button variant="subtle" c="red" onClick={rulesSheet.close}>
Close
<Trans>Close</Trans>
</Button>
</Stack>
</Sheet>
@@ -1,6 +1,7 @@
import { Anchor, Box, Group, Stack } from "@mantine/core";
import { CaretRightIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { Trans, useLingui } from "@lingui/react/macro";
import MatchCard from "@/features/matches/components/match-card";
import { Match } from "@/features/matches/types";
import SectionHeading from "./section-heading";
@@ -20,6 +21,7 @@ interface RecentResultsProps {
*/
const RecentResults = ({ matches, tournamentId, max = 5 }: RecentResultsProps) => {
const navigate = useNavigate();
const { t } = useLingui();
if (matches.length === 0) return null;
@@ -29,7 +31,7 @@ const RecentResults = ({ matches, tournamentId, max = 5 }: RecentResultsProps) =
return (
<Box>
<SectionHeading
label="Recently finished"
label={t`Recently finished`}
action={
hasMore ? (
<Anchor
@@ -42,7 +44,7 @@ const RecentResults = ({ matches, tournamentId, max = 5 }: RecentResultsProps) =
}
>
<Group gap={2} align="center" wrap="nowrap">
View all results
<Trans>View all results</Trans>
<CaretRightIcon size={12} weight="bold" />
</Group>
</Anchor>
@@ -1,4 +1,5 @@
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
import { Trans, useLingui } from "@lingui/react/macro";
import { Match } from "@/features/matches/types";
import { MatchReport } from "@/features/bracket/components/match-report";
import TeamAvatar from "@/components/team-avatar";
@@ -9,8 +10,8 @@ interface ScoreReportCtaProps {
matches: Match[];
}
const teamName = (t: Match["home"]): string =>
t && typeof t !== "string" ? t.name : "TBD";
const teamName = (team: Match["home"], fallback: string): string =>
team && typeof team !== "string" ? team.name : fallback;
/**
* Top-of-home call-to-action for a participant whose match is live. Composes
@@ -19,12 +20,14 @@ const teamName = (t: Match["home"]): string =>
* user isn't playing in a started match.
*/
const ScoreReportCta = ({ matches }: ScoreReportCtaProps) => {
const { t } = useLingui();
if (matches.length === 0) return null;
return (
<Box>
<SectionHeading
label="Your match is live"
label={t`Your match is live`}
live
count={matches.length}
/>
@@ -43,11 +46,11 @@ const ScoreReportCta = ({ matches }: ScoreReportCtaProps) => {
/>
)}
<Text size="sm" fw={600} lineClamp={1} style={{ minWidth: 0 }}>
{teamName(match.home)}
{teamName(match.home, t`TBD`)}
</Text>
</Group>
<Text size="xs" c="dimmed" fw={600} style={{ flexShrink: 0 }}>
vs
<Trans>vs</Trans>
</Text>
<Group
gap={8}
@@ -62,7 +65,7 @@ const ScoreReportCta = ({ matches }: ScoreReportCtaProps) => {
ta="right"
style={{ minWidth: 0 }}
>
{teamName(match.away)}
{teamName(match.away, t`TBD`)}
</Text>
{match.away && typeof match.away !== "string" && (
<TeamAvatar
@@ -1,4 +1,5 @@
import { Card, Group, Stack, Text, Avatar } from "@mantine/core";
import { Trans } from "@lingui/react/macro";
import { PlayerInfo } from "@/features/players/types";
import PlayerAvatar from "@/components/player-avatar";
@@ -19,7 +20,7 @@ const TeamAssignmentPreview: React.FC<TeamAssignmentPreviewProps> = ({ assignmen
<Card key={index} withBorder radius="md" p="md">
<Group gap="md" align="center" wrap="nowrap">
<Text size="sm" fw={600} c="dimmed" w={40}>
#{index + 1}
<Trans>#{index + 1}</Trans>
</Text>
<Group gap="sm" style={{ flex: 1 }} align="center">
@@ -30,7 +31,7 @@ const TeamAssignmentPreview: React.FC<TeamAssignmentPreviewProps> = ({ assignmen
</Group>
<Text size="lg" c="dimmed">
&
<Trans>&</Trans>
</Text>
<Group gap="sm" style={{ flex: 1 }} align="center">
@@ -3,12 +3,14 @@ import { useTournaments } from "../queries";
import { useSheet } from "@/hooks/use-sheet";
import { Button, Stack } from "@mantine/core";
import { PlusIcon, TrophyIcon } from "@phosphor-icons/react";
import { Trans, useLingui } from "@lingui/react/macro";
import Sheet from "@/components/sheet/sheet";
import TournamentForm from "./tournament-form";
import { TournamentCard } from "./tournament-card";
import EmptyState from "@/components/empty-state";
const TournamentCardList = () => {
const { t } = useLingui();
const { data: tournaments } = useTournaments();
const { roles } = useAuth();
const sheet = useSheet();
@@ -21,9 +23,9 @@ const TournamentCardList = () => {
variant="subtle"
onClick={sheet.open}
>
Create Tournament
<Trans>Create Tournament</Trans>
</Button>
<Sheet {...sheet.props} title="Create Tournament">
<Sheet {...sheet.props} title={t`Create Tournament`}>
<TournamentForm close={sheet.close} />
</Sheet>
</>
@@ -35,8 +37,8 @@ const TournamentCardList = () => {
) : (
<EmptyState
icon={<TrophyIcon size={32} />}
title="No Tournaments Yet"
description="The cups are racked and waiting. Check back soon."
title={t`No Tournaments Yet`}
description={t`The cups are racked and waiting. Check back soon.`}
/>
)}
</Stack>
@@ -12,6 +12,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { tournamentKeys } from "@/features/tournaments/queries";
import { useCallback } from "react";
import { DateTimePicker } from "@/components/date-time-picker";
import { useLingui } from "@lingui/react/macro";
interface TournamentFormProps {
close: () => void;
@@ -25,6 +26,7 @@ const TournamentForm = ({
tournamentId,
}: TournamentFormProps) => {
const isEditMode = !!tournamentId;
const { t, i18n } = useLingui();
const config: UseFormInput<TournamentInput> = {
initialValues: {
@@ -39,10 +41,10 @@ const TournamentForm = ({
},
onSubmitPreventDefault: "always",
validate: {
name: isNotEmpty("Name is required"),
location: isNotEmpty("Location is required"),
start_time: isNotEmpty("Start time is required"),
enroll_time: isNotEmpty("Enrollment time is required"),
name: isNotEmpty(t`Name is required`),
location: isNotEmpty(t`Location is required`),
start_time: isNotEmpty(t`Start time is required`),
enroll_time: isNotEmpty(t`Enrollment time is required`),
},
};
@@ -62,11 +64,11 @@ const TournamentForm = ({
const mutation = isEditMode ? updateTournament : createTournament;
const successMessage = isEditMode
? "Tournament updated successfully!"
: "Tournament created successfully!";
? t`Tournament updated successfully!`
: t`Tournament created successfully!`;
const errorMessage = isEditMode
? "Failed to update tournament"
: "Failed to create tournament";
? t`Failed to update tournament`
: t`Failed to create tournament`;
mutation(tournamentData, {
onSuccess: async (tournament) => {
@@ -83,7 +85,7 @@ const TournamentForm = ({
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to upload logo");
throw new Error(error.error || t`Failed to upload logo`);
}
const result = await response.json();
@@ -100,8 +102,8 @@ const TournamentForm = ({
toast.success(successMessage);
} catch (error: any) {
const logoErrorMessage = isEditMode
? `Tournament updated but logo upload failed: ${error.message}`
: `Tournament created but logo upload failed: ${error.message}`;
? t`Tournament updated but logo upload failed: ${error.message}`
: t`Tournament created but logo upload failed: ${error.message}`;
toast.error(logoErrorMessage);
logger.error("Tournament logo upload error", error);
}
@@ -126,33 +128,33 @@ const TournamentForm = ({
<SlidePanel
onSubmit={form.onSubmit(handleSubmit)}
onCancel={close}
submitText={isEditMode ? "Update Tournament" : "Create Tournament"}
cancelText="Cancel"
submitText={isEditMode ? t`Update Tournament` : t`Create Tournament`}
cancelText={t`Cancel`}
loading={isPending}
>
<Stack>
<TextInput
label="Name"
label={t`Name`}
withAsterisk
key={form.key("name")}
{...form.getInputProps("name")}
/>
<TextInput
label="Location"
label={t`Location`}
withAsterisk
key={form.key("location")}
{...form.getInputProps("location")}
/>
<Textarea
label="Description"
label={t`Description`}
key={form.key("desc")}
{...form.getInputProps("desc")}
minRows={3}
/>
<Checkbox
label="Regional Tournament"
label={t`Regional Tournament`}
key={form.key("regional")}
{...form.getInputProps("regional", { type: "checkbox" })}
/>
@@ -160,7 +162,7 @@ const TournamentForm = ({
<FileInput
key={form.key("logo")}
accept="image/png,image/jpeg,image/gif,image/jpg"
label={isEditMode ? "Change Logo" : "Logo"}
label={isEditMode ? t`Change Logo` : t`Logo`}
leftSection={<LinkIcon size={16} />}
{...form.getInputProps("logo")}
/>
@@ -169,12 +171,12 @@ const TournamentForm = ({
key={form.key("start_time")}
{...form.getInputProps("start_time")}
Component={DateTimePicker}
title="Select Start Date"
label="Start Date"
title={t`Select Start Date`}
label={t`Start Date`}
withAsterisk
formatValue={(date) =>
!date ? 'Select a time' :
new Date(date).toLocaleDateString("en-US", {
!date ? t`Select a time` :
new Date(date).toLocaleDateString(i18n.locale, {
weekday: "short",
year: "numeric",
month: "short",
@@ -190,12 +192,12 @@ const TournamentForm = ({
key={form.key("enroll_time")}
{...form.getInputProps("enroll_time")}
Component={DateTimePicker}
title="Select Enrollment Due Date"
label="Enrollment Due"
title={t`Select Enrollment Due Date`}
label={t`Enrollment Due`}
withAsterisk
formatValue={(date) =>
!date ? 'Select a time' :
new Date(date).toLocaleDateString("en-US", {
!date ? t`Select a time` :
new Date(date).toLocaleDateString(i18n.locale, {
weekday: "short",
year: "numeric",
month: "short",
@@ -212,11 +214,11 @@ const TournamentForm = ({
key={form.key("end_time")}
{...form.getInputProps("end_time")}
Component={DateTimePicker}
title="Select End Date"
label="End Date (Optional)"
title={t`Select End Date`}
label={t`End Date (Optional)`}
formatValue={(date) =>
!date ? 'Select a time' :
new Date(date).toLocaleDateString("en-US", {
!date ? t`Select a time` :
new Date(date).toLocaleDateString(i18n.locale, {
weekday: "short",
year: "numeric",
month: "short",
@@ -5,6 +5,7 @@ import { TournamentInfo } from "../types";
import { Fragment, useCallback } from "react";
import React from "react";
import { TrophyIcon, CalendarIcon, MapPinIcon } from "@phosphor-icons/react";
import { Trans, useLingui } from "@lingui/react/macro";
interface TournamentListProps {
tournaments: TournamentInfo[];
@@ -16,6 +17,7 @@ interface TournamentListItemProps {
}
const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps) => {
const { i18n } = useLingui();
const startDate = tournament.start_time ? new Date(tournament.start_time) : null;
return (
@@ -41,7 +43,7 @@ const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps)
<CalendarIcon size={10} />
</ThemeIcon>
<Text size="xs" c="dimmed">
{startDate.toLocaleDateString(undefined, {
{startDate.toLocaleDateString(i18n.locale, {
month: 'short',
day: 'numeric',
year: 'numeric'
@@ -90,7 +92,7 @@ const TournamentList = ({ tournaments, loading = false }: TournamentListProps) =
<TrophyIcon size={32} />
</ThemeIcon>
<Text c="dimmed" size="lg">
No tournaments found
<Trans>No tournaments found</Trans>
</Text>
</Box>
);
@@ -18,12 +18,14 @@ import TeamAvatar from "@/components/team-avatar";
import ListLink from "@/components/list-link";
import { isTournamentPredictable } from "@/features/predictions/utils";
import { Podium } from "./podium";
import { Trans, useLingui } from "@lingui/react/macro";
interface TournamentStatsProps {
tournament: Tournament;
}
export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
const { t } = useLingui();
const matches = tournament.matches || [];
const nonByeMatches = useMemo(() =>
@@ -62,7 +64,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
<Box p="md">
<Center>
<Text c="dimmed" size="sm">
No stats available yet
<Trans>No stats available yet</Trans>
</Text>
</Center>
</Box>
@@ -71,8 +73,8 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
return (
<Stack gap={0}>
<Text px="md" size="lg" fw={600}>Results</Text>
<Text px="md" c="dimmed" size="xs" fw={500}>Sorted by win percentage</Text>
<Text px="md" size="lg" fw={600}><Trans>Results</Trans></Text>
<Text px="md" c="dimmed" size="xs" fw={500}><Trans>Sorted by win percentage</Trans></Text>
{teamStatsWithCalculations.map((stat, index) => {
const team = tournament.teams?.find(t => t.id === stat.team_id);
@@ -107,7 +109,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
<Group gap="md" ta="center">
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
W
<Trans>W</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.wins}
@@ -115,7 +117,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
L
<Trans>L</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.losses}
@@ -123,7 +125,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
W%
<Trans>W%</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.winPercentage.toFixed(1)}%
@@ -131,7 +133,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
AVG
<Trans>AVG</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.avgCupsPerMatch.toFixed(1)}
@@ -139,7 +141,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
CF
<Trans>CF</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.total_cups_made}
@@ -147,7 +149,7 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
</Stack>
<Stack gap={0}>
<Text size="xs" c="dimmed" fw={700}>
CA
<Trans>CA</Trans>
</Text>
<Text size="xs" c="dimmed">
{stat.total_cups_against}
@@ -170,27 +172,27 @@ export const TournamentStats = memo(({ tournament }: TournamentStatsProps) => {
<Container size="100%" px={0}>
<Stack gap="md">
{tournament.regional && !hasGroupStage && (
<Alert px="md" variant="light" title="Regional Tournament" icon={<InfoIcon size={16} />}>
Earlier regional formats aren't supported in the app and order of matches or displayed winners may be unreliable.
<Alert px="md" variant="light" title={t`Regional Tournament`} icon={<InfoIcon size={16} />}>
<Trans>Earlier regional formats aren't supported in the app and order of matches or displayed winners may be unreliable.</Trans>
</Alert>
)}
{!tournament.regional && <Podium tournament={tournament} />}
<Stack gap={0}>
{hasGroupStage && (
<ListLink
label={`View Groups`}
label={t`View Groups`}
to={`/tournaments/${tournament.id}/groups`}
Icon={ListDashes}
/>
)}
<ListLink
label={`View Bracket`}
label={t`View Bracket`}
to={`/tournaments/${tournament.id}/bracket`}
Icon={TreeStructureIcon}
/>
{isTournamentPredictable(tournament) && (
<ListLink
label={`View Predictions`}
label={t`View Predictions`}
to={`/tournaments/${tournament.id}/predictions`}
Icon={WizardOrbIcon}
/>
@@ -4,10 +4,12 @@ import { useAuth } from "@/contexts/auth-context";
import { useSheet } from "@/hooks/use-sheet";
import { Stack, Text } from "@mantine/core";
import useEnrollFreeAgent from "../../hooks/use-enroll-free-agent";
import { Trans, useLingui } from "@lingui/react/macro";
const EnrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string, isRegional?: boolean} ) => {
const { open, isOpen, toggle } = useSheet();
const { user, phone } = useAuth();
const { t } = useLingui();
const { mutate: enrollFreeAgent, isPending: isEnrolling } = useEnrollFreeAgent(isRegional);
const handleEnroll = () => {
@@ -21,32 +23,32 @@ const EnrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string, is
return (
<>
<Button variant="subtle" size="sm" onClick={open}>
{isRegional ? "Enroll" : "Enroll As Free Agent"}
{isRegional ? <Trans>Enroll</Trans> : <Trans>Enroll As Free Agent</Trans>}
</Button>
<Sheet title={isRegional ? "Enrollment" : "Free Agent Enrollment"} opened={isOpen} onChange={toggle}>
<Sheet title={isRegional ? t`Enrollment` : t`Free Agent Enrollment`} opened={isOpen} onChange={toggle}>
<Stack gap="xs">
{isRegional ? (
<>
<Text size="md">
Enroll in this regional tournament to be assigned a random partner.
<Trans>Enroll in this regional tournament to be assigned a random partner.</Trans>
</Text>
</>
) : (
<>
<Text size="md">
Enrolling as a free agent adds you to a pool of players looking for teammates.
<Trans>Enrolling as a free agent adds you to a pool of players looking for teammates.</Trans>
</Text>
<Text size="sm" c='dimmed'>
Once enrolled, you can view other free agents and their phone number in order to coordinate teams and walkout songs.
<Trans>Once enrolled, you can view other free agents and their phone number in order to coordinate teams and walkout songs.</Trans>
</Text>
<Text size="xs" c="dimmed">
Important: Enrolling as a free agent does not guarantee a tournament spot. To secure a spot, one team member must register through the app and select a walkout song.
<Trans>Important: Enrolling as a free agent does not guarantee a tournament spot. To secure a spot, one team member must register through the app and select a walkout song.</Trans>
</Text>
</>
)}
<Button onClick={handleEnroll} loading={isEnrolling}>Confirm</Button>
<Button variant="subtle" color="red" onClick={toggle}>Cancel</Button>
<Button onClick={handleEnroll} loading={isEnrolling}><Trans>Confirm</Trans></Button>
<Button variant="subtle" color="red" onClick={toggle}><Trans>Cancel</Trans></Button>
</Stack>
</Sheet>
</>
@@ -11,6 +11,7 @@ import { useQuery } from "@tanstack/react-query";
import { Loader } from "@mantine/core";
import useEnrollTeam from "@/features/tournaments/hooks/use-enroll-team";
import { useServerQuery } from "@/lib/tanstack-query/hooks";
import { Trans, useLingui } from "@lingui/react/macro";
interface EnrollTeamProps {
tournamentId: string;
@@ -20,6 +21,7 @@ interface EnrollTeamProps {
const EnrollTeam = ({ tournamentId, onSubmit }: EnrollTeamProps) => {
const { open, isOpen, toggle } = useSheet();
const { user } = useAuth();
const { t } = useLingui();
const hasTeams = useMemo(() => !!user?.teams?.length, [user?.teams]);
const [selectedTeamId, setSelectedTeamId] = useState<string>();
@@ -88,10 +90,10 @@ const EnrollTeam = ({ tournamentId, onSubmit }: EnrollTeamProps) => {
return (
<>
<Button size="sm" onClick={open}>
Enroll Your Team
<Trans>Enroll Your Team</Trans>
</Button>
<Sheet title={showTeamForm ? "Team Details" : "Enroll Team"} opened={isOpen} onChange={toggle}>
<Sheet title={showTeamForm ? t`Team Details` : t`Enroll Team`} opened={isOpen} onChange={toggle}>
{ isEnrolling && <Loader size="lg" /> }
{showTeamForm && !isEnrolling ? (
<TeamForm
@@ -108,7 +110,7 @@ const EnrollTeam = ({ tournamentId, onSubmit }: EnrollTeamProps) => {
onSelect={handleSelect}
/>
<Button mt='sm' variant="subtle" color='red' size="sm" onClick={toggle}>
Cancel
<Trans>Cancel</Trans>
</Button>
</>
)}
@@ -2,6 +2,7 @@ import { Stack, Button, Divider, Group, ComboboxItem, Text } from '@mantine/core
import { PlusIcon } from '@phosphor-icons/react';
import React, { useMemo } from 'react';
import Typeahead, { TypeaheadOption } from '@/components/typeahead';
import { Trans, useLingui } from '@lingui/react/macro';
interface TeamSelectionViewProps {
options: ComboboxItem[];
@@ -13,6 +14,7 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
onSelect
}) => {
const [selectedTeam, setSelectedTeam] = React.useState<ComboboxItem | null>(null);
const { t } = useLingui();
const searchTeams = async (query: string): Promise<TypeaheadOption<ComboboxItem>[]> => {
const filtered = options.filter(option =>
@@ -52,14 +54,14 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
variant="light"
fullWidth
>
Create New Team
<Trans>Create New Team</Trans>
</Button>
<Divider my="sm" label="or" />
<Divider my="sm" label={t`or`} />
<Stack gap="sm">
<Typeahead
placeholder="Select one of your existing teams"
placeholder={t`Select one of your existing teams`}
onSelect={handleTeamSelect}
searchFn={searchTeams}
renderOption={renderTeamOption}
@@ -72,7 +74,7 @@ const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
disabled={!selectedTeam}
fullWidth
>
Enroll Selected Team
<Trans>Enroll Selected Team</Trans>
</Button>
</Stack>
</Stack>
@@ -1,5 +1,6 @@
import { Group, Stack, Text, Card, Badge, Box, ActionIcon } from "@mantine/core";
import { PhoneIcon, CheckCircleIcon } from "@phosphor-icons/react";
import { Trans, useLingui } from "@lingui/react/macro";
import { useFreeAgents } from "../../queries";
import UnenrollFreeAgent from "./unenroll-free-agent";
import toast from "@/lib/sonner";
@@ -12,12 +13,13 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
}) => {
const { data: freeAgents } = useFreeAgents(tournamentId);
const { user } = useAuth();
const { t } = useLingui();
const copyToClipboard = async (phone: string) => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(phone);
toast.success("Phone number copied!");
toast.success(t`Phone number copied!`);
return;
}
@@ -32,13 +34,13 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
document.body.removeChild(textArea);
if (successful) {
toast.success("Phone number copied!");
toast.success(t`Phone number copied!`);
} else {
throw new Error("Copy command failed");
}
} catch (err) {
console.error("Failed to copy:", err);
toast.error("Failed to copy");
toast.error(t`Failed to copy`);
}
};
@@ -56,7 +58,7 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
<Group gap={4} align="center">
<CheckCircleIcon size={14} weight="fill" color="var(--mantine-color-green-6)" />
<Text size="xs" c="green" fw={500}>
Enrolled
<Trans>Enrolled</Trans>
</Text>
</Group>
</Box>
@@ -65,7 +67,7 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
</Card>
<Text size="xs" c="dimmed" ta="center">
Partners will be randomly assigned when enrollment closes
<Trans>Partners will be randomly assigned when enrollment closes</Trans>
</Text>
<UnenrollFreeAgent tournamentId={tournamentId} isRegional={isRegional} />
@@ -76,18 +78,18 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
return (
<Stack gap="sm">
<Text size="sm" fw={600} c="green">
✓ Enrolled as Free Agent
<Trans>✓ Enrolled as Free Agent</Trans>
</Text>
<Text size="xs" c="dimmed">
Other players looking for teammates:
<Trans>Other players looking for teammates:</Trans>
</Text>
{freeAgents.length > 1 ? (
<Stack gap="xs">
<Group gap="xs" align="center">
<Text size="xs" fw={500} c="dimmed">
Free Agents
<Trans>Free Agents</Trans>
</Text>
<Badge variant="light" size="xs" color="blue">
{freeAgents.length}
@@ -107,7 +109,7 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
<ActionIcon
variant="subtle"
size="sm"
aria-label="Copy phone number"
aria-label={t`Copy phone number`}
onClick={() => copyToClipboard(agent.phone!)}
style={{ cursor: 'pointer' }}
>
@@ -129,7 +131,7 @@ const EnrolledFreeAgent: React.FC<{ tournamentId: string, isRegional?: boolean }
</Stack>
) : (
<Text size="xs" c="dimmed" py="sm">
You're the first free agent!
<Trans>You're the first free agent!</Trans>
</Text>
)}
@@ -1,6 +1,8 @@
import Sheet from "@/components/sheet/sheet";
import { useSheet } from "@/hooks/use-sheet";
import { UserListIcon } from "@phosphor-icons/react";
import { Trans, useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
import { useMemo } from "react";
import { useFreeAgents } from "../../queries";
import { Text } from "@mantine/core";
@@ -16,6 +18,7 @@ const EnrolledPlayersListButton: React.FC<EnrolledPlayersListButtonProps> = ({ t
const { data: freeAgents } = useFreeAgents(tournamentId);
const count = useMemo(() => freeAgents.length, [freeAgents]);
const { open, isOpen, toggle } = useSheet();
const { t, i18n } = useLingui();
const players = useMemo(() =>
freeAgents.map(agent => agent.player).filter((player): player is Player => player !== undefined),
@@ -24,12 +27,19 @@ const EnrolledPlayersListButton: React.FC<EnrolledPlayersListButtonProps> = ({ t
return (
<>
<NavTile label={`Players (${count})`} Icon={UserListIcon} onClick={open} />
<NavTile
label={i18n._({
...msg`{count, plural, one {Players (#)} other {Players (#)}}`,
values: { count },
})}
Icon={UserListIcon}
onClick={open}
/>
<Sheet title="Enrolled Players" opened={isOpen} onChange={toggle}>
<Sheet title={t`Enrolled Players`} opened={isOpen} onChange={toggle}>
{count === 0 ? (
<Text size="sm" c="dimmed" ta="center" py="xl">
No players enrolled yet
<Trans>No players enrolled yet</Trans>
</Text>
) : (
<PlayerList players={players} />
@@ -1,4 +1,5 @@
import { Suspense, useMemo } from "react";
import { Trans, useLingui } from "@lingui/react/macro";
import { Tournament } from "../../types";
import { useAuth } from "@/contexts/auth-context";
import { Box, Divider, Paper, SimpleGrid, Stack, Text } from "@mantine/core";
@@ -27,6 +28,7 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
}) => {
const { user, roles } = useAuth();
const navigate = useNavigate();
const { t } = useLingui();
const isAdmin = useMemo(() => roles.includes("Admin"), [roles]);
const userTeam = useMemo(
@@ -63,7 +65,7 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
<Box>
<SectionHeading
label="Enrollment"
label={t`Enrollment`}
action={
isEnrollmentOpen ? (
<Countdown date={enrollmentDeadline} />
@@ -75,7 +77,7 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
<Stack gap="sm">
{!isUserEnrolled && !isEnrollmentOpen && (
<Text fw={600} c="dimmed" size="sm">
Enrollment has been closed for this tournament.
<Trans>Enrollment has been closed for this tournament.</Trans>
</Text>
)}
@@ -87,7 +89,7 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
tournamentId={tournament.id}
onSubmit={handleSubmit}
/>
<Divider my={0} label="or" />
<Divider my={0} label={t`or`} />
</>
)}
<EnrollFreeAgent
@@ -138,14 +140,14 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
<SimpleGrid cols={3} spacing="sm" verticalSpacing="sm">
{isAdmin && (
<NavTile
label="Manage"
label={t`Manage`}
Icon={UsersIcon}
onClick={() =>
navigate({ to: `/admin/tournaments/${tournament.id}` })
}
/>
)}
<NavTile label="Bracket" Icon={TreeStructureIcon} disabled />
<NavTile label={t`Bracket`} Icon={TreeStructureIcon} disabled />
{tournament.regional === true ? (
(tournament.teams && tournament.teams.length > 0) ? (
<TeamListButton teams={tournament.teams} isRegional={true} />
@@ -1,6 +1,7 @@
import Sheet from "@/components/sheet/sheet"
import { useSheet } from "@/hooks/use-sheet"
import { ListIcon } from "@phosphor-icons/react"
import { Trans, useLingui } from "@lingui/react/macro"
import { useTournament } from "../../queries"
import { Button, Flex, Loader, Stack } from "@mantine/core"
import { lazy, Suspense } from "react"
@@ -15,12 +16,13 @@ interface RulesListButtonProps {
const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
const { data: tournament } = useTournament(tournamentId);
const { open, isOpen, toggle } = useSheet();
const { t } = useLingui();
return (
<>
<NavTile label="Rules" Icon={ListIcon} onClick={open} />
<NavTile label={t`Rules`} Icon={ListIcon} onClick={open} />
<Sheet title="Tournament Rules" opened={isOpen} onChange={toggle}>
<Sheet title={t`Tournament Rules`} opened={isOpen} onChange={toggle}>
<Stack gap="xs">
<Suspense
fallback={
@@ -31,7 +33,7 @@ const RulesListButton: React.FC<RulesListButtonProps> = ({ tournamentId }) => {
>
<RulesContent content={tournament?.rules || ''} />
</Suspense>
<Button variant="subtle" c="red" onClick={toggle}>Close</Button>
<Button variant="subtle" c="red" onClick={toggle}><Trans>Close</Trans></Button>
</Stack>
</Sheet>
</>
@@ -4,6 +4,8 @@ import { TeamInfo } from "@/features/teams/types"
import { useSheet } from "@/hooks/use-sheet"
import { UsersIcon } from "@phosphor-icons/react"
import { useMemo } from "react"
import { useLingui } from "@lingui/react/macro"
import { msg } from "@lingui/core/macro"
import { NavTile } from "../started-tournament/sections/nav-grid"
interface TeamListButtonProps {
@@ -12,13 +14,21 @@ interface TeamListButtonProps {
}
const TeamListButton: React.FC<TeamListButtonProps> = ({ teams, isRegional }) => {
const { t, i18n } = useLingui();
const count = useMemo(() => teams.length, [teams]);
const { open, isOpen, toggle } = useSheet();
return (
<>
<NavTile label={`Teams (${count})`} Icon={UsersIcon} onClick={open} />
<NavTile
label={i18n._({
...msg`{count, plural, one {Teams (#)} other {Teams (#)}}`,
values: { count },
})}
Icon={UsersIcon}
onClick={open}
/>
<Sheet title="Enrolled Teams" opened={isOpen} onChange={toggle}>
<Sheet title={t`Enrolled Teams`} opened={isOpen} onChange={toggle}>
<TeamList teams={teams} isRegional={isRegional} />
</Sheet>
</>
@@ -3,9 +3,11 @@ import Sheet from "@/components/sheet/sheet";
import { useAuth } from "@/contexts/auth-context";
import { useSheet } from "@/hooks/use-sheet";
import { Stack, Text } from "@mantine/core";
import { Trans, useLingui } from "@lingui/react/macro";
import useUnenrollFreeAgent from "../../hooks/use-unenroll-free-agent";
const UnenrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string, isRegional?: boolean} ) => {
const { t } = useLingui();
const { open, isOpen, toggle } = useSheet();
const { user } = useAuth();
@@ -21,20 +23,20 @@ const UnenrollFreeAgent = ({ tournamentId, isRegional }: {tournamentId: string,
return (
<>
<Button variant="subtle" size="sm" color="red" onClick={open}>
Unenroll
<Trans>Unenroll</Trans>
</Button>
<Sheet title="Unenroll from tournament?" opened={isOpen} onChange={toggle}>
<Sheet title={t`Unenroll from tournament?`} opened={isOpen} onChange={toggle}>
<Stack gap="xs">
<Text size="md">
{isRegional
? "This will remove you from the tournament enrollment."
: "This will remove you from the free agent list."}
? <Trans>This will remove you from the tournament enrollment.</Trans>
: <Trans>This will remove you from the free agent list.</Trans>}
</Text>
<Button color="red" onClick={handleUnenroll} loading={isEnrolling}>
Confirm Unenrollment
<Trans>Confirm Unenrollment</Trans>
</Button>
<Button variant="subtle" onClick={toggle}>Cancel</Button>
<Button variant="subtle" onClick={toggle}><Trans>Cancel</Trans></Button>
</Stack>
</Sheet>
</>
@@ -4,6 +4,7 @@ import { useSheet } from "@/hooks/use-sheet";
import { useCallback } from "react";
import useUnenrollTeam from "../../hooks/use-unenroll-team";
import { Stack, Text } from "@mantine/core";
import { Trans, useLingui } from "@lingui/react/macro";
interface UnenrollTeamProps {
tournamentId: string;
@@ -12,6 +13,7 @@ interface UnenrollTeamProps {
}
const UnenrollTeam = ({ tournamentId, teamId, onSubmit }: UnenrollTeamProps) => {
const { t } = useLingui();
const { open, isOpen, toggle } = useSheet();
const { mutate: unenrollTeam, isPending } = useUnenrollTeam();
const handleUnenrollTeam = useCallback(
@@ -29,14 +31,14 @@ const UnenrollTeam = ({ tournamentId, teamId, onSubmit }: UnenrollTeamProps) =>
return (
<>
<Button size="sm" onClick={open} variant="subtle">
Unenroll
<Trans>Unenroll</Trans>
</Button>
<Sheet title={"Unenroll Team"} opened={isOpen} onChange={toggle}>
<Sheet title={t`Unenroll Team`} opened={isOpen} onChange={toggle}>
<Stack gap='xs'>
<Text size="sm" fw={500}>Are you sure you want to unenroll from this tournament? You can enroll again at any point before the deadline.</Text>
<Button onClick={handleUnenrollTeam} loading={isPending}>Confirm</Button>
<Button variant="subtle" color="red" onClick={toggle} disabled={isPending}>Cancel</Button>
<Text size="sm" fw={500}><Trans>Are you sure you want to unenroll from this tournament? You can enroll again at any point before the deadline.</Trans></Text>
<Button onClick={handleUnenrollTeam} loading={isPending}><Trans>Confirm</Trans></Button>
<Button variant="subtle" color="red" onClick={toggle} disabled={isPending}><Trans>Cancel</Trans></Button>
</Stack>
</Sheet>
</>
@@ -10,6 +10,7 @@ import { Team } from "@/features/teams/types";
import { useQuery } from "@tanstack/react-query";
import { Loader } from "@mantine/core";
import useEnrollTeam from "@/features/tournaments/hooks/use-enroll-team";
import { Trans, useLingui } from "@lingui/react/macro";
interface UpdateTeamProps {
tournamentId: string;
@@ -17,6 +18,7 @@ interface UpdateTeamProps {
}
const UpdateTeam = ({ tournamentId, teamId }: UpdateTeamProps) => {
const { t } = useLingui();
const { open, isOpen, toggle } = useSheet();
const { data: team } = useTeam(teamId);
@@ -39,10 +41,10 @@ const UpdateTeam = ({ tournamentId, teamId }: UpdateTeamProps) => {
return (
<>
<Button size="sm" onClick={open}>
Update Team
<Trans>Update Team</Trans>
</Button>
<Sheet title={"Update Team"} opened={isOpen} onChange={toggle}>
<Sheet title={t`Update Team`} opened={isOpen} onChange={toggle}>
<TeamForm
close={toggle}
tournamentId={tournamentId}