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
@@ -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}