work on team enrollment

This commit is contained in:
yohlo
2025-09-16 09:24:21 -05:00
parent 9a105b30c6
commit cde74a04d5
45 changed files with 1244 additions and 457 deletions

View File

@@ -0,0 +1,29 @@
import Button from "@/components/button";
import Sheet from "@/components/sheet/sheet";
import { useAuth } from "@/contexts/auth-context";
import { useSheet } from "@/hooks/use-sheet";
import { Text } from "@mantine/core";
const EnrollFreeAgent = () => {
const { open, isOpen, toggle } = useSheet();
const { user } = useAuth();
return (
<>
<Button variant="subtle" size="sm" onClick={open}>
Enroll As Free Agent
</Button>
<Sheet title="Free Agent Enrollment" opened={isOpen} onChange={toggle}>
<Text size="md" mb="md">
Enrolling as a free agent will enter you in a pool of players wanting to play but don't have a teammate yet.
</Text>
<Text size="sm" mb="md" c='dimmed'>
You will be automatically paired with a partner before the tournament starts, and you will be able to see your new team and set a walkout song in the app.
</Text>
<Button onClick={console.log}>Confirm</Button>
</Sheet>
</>
);
};
export default EnrollFreeAgent;

View File

@@ -0,0 +1,61 @@
import Button from "@/components/button";
import Sheet from "@/components/sheet/sheet";
import { useAuth } from "@/contexts/auth-context";
import { useSheet } from "@/hooks/use-sheet";
import { useMemo, useState, useCallback } from "react";
import TeamSelectionView from "./team-selection-view";
import TeamForm from "@/features/teams/components/team-form";
interface EnrollTeamProps {
tournamentId: string;
}
const EnrollTeam = ({ tournamentId }: EnrollTeamProps) => {
const { open, isOpen, toggle } = useSheet();
const { user } = useAuth();
const hasTeams = useMemo(() => !!user?.teams?.length, [user?.teams]);
const [selectedTeamId, setSelectedTeamId] = useState<string>();
const [showTeamForm, setShowTeamForm] = useState<boolean>(!!hasTeams);
const teamOptions = useMemo(() =>
user?.teams?.map(team => ({
value: team.id,
label: team.name
})) || [],
[user?.teams]
);
const handleSelect = useCallback((teamId: string | undefined) => {
setSelectedTeamId(teamId);
setShowTeamForm(true);
}, []);
return (
<>
<Button size="sm" onClick={open}>
Enroll Your Team
</Button>
<Sheet title={showTeamForm ? "Team Details" : "Enroll Team"} opened={!isOpen} onChange={toggle}>
{showTeamForm ? (
<TeamForm close={console.log} tournamentId={tournamentId} />
) : (
<>
<TeamSelectionView
options={teamOptions}
onSelect={handleSelect}
/>
<Button mt='sm' variant="subtle" color='red' size="sm" onClick={toggle}>
Cancel
</Button>
</>
)}
</Sheet>
</>
);
};
export default EnrollTeam;

View File

@@ -0,0 +1,57 @@
import { Stack, Button, Divider, Autocomplete, Group, ComboboxItem } from '@mantine/core';
import { PlusIcon } from '@phosphor-icons/react';
import React, { useMemo, useState } from 'react';
interface TeamSelectionViewProps {
options: ComboboxItem[];
onSelect: (teamId: string | undefined) => void;
}
const TeamSelectionView: React.FC<TeamSelectionViewProps> = React.memo(({
options,
onSelect
}) => {
const [value, setValue] = useState<string>();
const selectedOption = useMemo(() => options.find(option => option.label === value), [value])
const handleCreateNewTeamClicked = () => onSelect(undefined);
const handleSelectExistingTeam = () => onSelect(selectedOption?.value)
return (
<Stack gap="md">
<Button
leftSection={<PlusIcon weight="bold" />}
onClick={handleCreateNewTeamClicked}
variant="light"
fullWidth
>
Create New Team
</Button>
<Divider my="sm" label="or" />
<Stack gap="sm">
<Autocomplete
placeholder="Select one of your existing teams"
value={selectedOption?.label || ''}
onChange={setValue}
data={options.map(option => option.label)}
comboboxProps={{ withinPortal: false }}
/>
<Button
onClick={handleSelectExistingTeam}
disabled={!selectedOption}
fullWidth
>
Enroll Selected Team
</Button>
</Stack>
</Stack>
);
});
TeamSelectionView.displayName = 'TeamSelectionView';
export default TeamSelectionView;

View File

@@ -1,72 +1,81 @@
import { Group, Stack, ThemeIcon, Text } from "@mantine/core";
import { Tournament } from "../../types";
import Avatar from "@/components/avatar";
import { CalendarIcon, MapPinIcon, TrophyIcon, UsersIcon } from "@phosphor-icons/react";
import {
CalendarIcon,
MapPinIcon,
TrophyIcon,
UsersIcon,
} from "@phosphor-icons/react";
import { useMemo } from "react";
const Header = ({ tournament }: { tournament: Tournament}) => {
const tournamentStart = useMemo(() => new Date(tournament.start_time), [tournament.start_time]);
const teamCount = useMemo(() => tournament.teams?.length || 0, [tournament.teams]);
const Header = ({ tournament }: { tournament: Tournament }) => {
const tournamentStart = useMemo(
() => new Date(tournament.start_time),
[tournament.start_time]
);
const teamCount = useMemo(
() => tournament.teams?.length || 0,
[tournament.teams]
);
return (
<Group justify="space-around" align="flex-start" w='100%'>
<Group align="center" gap="lg">
<Avatar
name={tournament.name}
src={
tournament.logo
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
: undefined
}
radius="md"
size={200}
px='md'
withBorder={false}
>
<TrophyIcon size={32} />
</Avatar>
<Stack gap="xs">
{tournament.location && (
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<MapPinIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournament.location}
</Text>
</Group>
)}
return (
<Stack align="center" gap="lg">
<Avatar
name={tournament.name}
src={
tournament.logo
? `/api/files/tournaments/${tournament.id}/${tournament.logo}`
: undefined
}
radius="md"
size={200}
px="xs"
withBorder={false}
>
<TrophyIcon size={32} />
</Avatar>
<Stack gap="xs">
{tournament.location && (
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<MapPinIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournament.location}
</Text>
</Group>
)}
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<CalendarIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournamentStart.toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
})}{" "}
at{" "}
{tournamentStart.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</Group>
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<UsersIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{teamCount} teams enrolled
</Text>
</Group>
</Stack>
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<CalendarIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{tournamentStart.toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric",
})}{" "}
at{" "}
{tournamentStart.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</Group>
</Group>
)
}
export default Header;
<Group gap="xs">
<ThemeIcon size="sm" variant="light" radius="sm">
<UsersIcon size={14} />
</ThemeIcon>
<Text size="sm" c="dimmed">
{teamCount} teams enrolled
</Text>
</Group>
</Stack>
</Stack>
);
};
export default Header;

View File

@@ -1,30 +1,23 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { Tournament } from "../../types";
import { useAuth } from "@/contexts/auth-context";
import {
Box,
Button,
Card,
Divider,
Group,
Stack,
Title,
Text,
ThemeIcon,
} from "@mantine/core";
import Avatar from "@/components/avatar";
import Countdown from "@/components/countdown";
import ListLink from "@/components/list-link";
import ListButton from "@/components/list-button";
import {
TrophyIcon,
CalendarIcon,
MapPinIcon,
UsersIcon,
ListIcon,
} from "@phosphor-icons/react";
import EnrollTeam from "../enroll-team";
import EnrollFreeAgent from "../enroll-free-agent";
import EnrollTeam from "./enroll-team";
import EnrollFreeAgent from "./enroll-free-agent";
import TeamListButton from "./team-list-button";
import Header from "./header";
@@ -56,8 +49,8 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
{tournament.desc && <Text size="sm">{tournament.desc}</Text>}
<Card withBorder radius="md" p="lg">
<Stack gap="md">
<Group gap="xs" align="center">
<Stack gap="xs">
<Group mb='sm' gap="xs" align="center">
<UsersIcon size={16} />
<Text size="sm" fw={500}>
Enrollment
@@ -73,10 +66,14 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
)}
</Group>
{!isEnrollmentOpen && !isUserEnrolled && (
<Text fw={600} c="dimmed" size="sm">Enrollment has been closed for this tournament.</Text>
)}
{!isUserEnrolled && isEnrollmentOpen && (
<>
<EnrollTeam />
<Divider label="or" />
<EnrollTeam tournamentId={tournament.id} />
<Divider my={0} label="or" />
<EnrollFreeAgent />
</>
)}