i18n
This commit is contained in:
@@ -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>
|
||||
|
||||
+9
-6
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
+6
-4
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+13
-3
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user