work on team enrollment
This commit is contained in:
80
src/features/teams/components/team-form/color-picker.tsx
Normal file
80
src/features/teams/components/team-form/color-picker.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ColorPicker, TextInput, Stack, Group, ColorSwatch, Text } from '@mantine/core';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
const presetColors = [
|
||||
'#FF0000', '#00FF00', '#0000FF', '#FFFF00',
|
||||
'#FF00FF', '#00FFFF', '#FFA500', '#800080',
|
||||
'#008000', '#000080', '#800000', '#808000'
|
||||
];
|
||||
|
||||
interface TeamColorPickerProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const TeamColorPicker: React.FC<TeamColorPickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
label = "Select Color"
|
||||
}) => {
|
||||
const [customHex, setCustomHex] = useState(value || '');
|
||||
|
||||
const isValidHex = useMemo(() => {
|
||||
const hexRegex = /^#[0-9A-F]{6}$/i;
|
||||
return hexRegex.test(customHex);
|
||||
}, [customHex]);
|
||||
|
||||
const handleColorChange = useCallback((color: string) => {
|
||||
setCustomHex(color);
|
||||
onChange(color);
|
||||
}, [onChange]);
|
||||
|
||||
const handleHexInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const hex = event.currentTarget.value;
|
||||
setCustomHex(hex);
|
||||
|
||||
if (/^#[0-9A-F]{6}$/i.test(hex)) {
|
||||
onChange(hex);
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Stack gap="md" p="md" w="100%">
|
||||
<Text fw={500}>{label}</Text>
|
||||
|
||||
<ColorPicker
|
||||
value={value || '#000000'}
|
||||
onChange={handleColorChange}
|
||||
format="hex"
|
||||
swatches={presetColors}
|
||||
withPicker={true}
|
||||
fullWidth
|
||||
size="md"
|
||||
swatchesPerRow={12}
|
||||
/>
|
||||
|
||||
<Group gap="xs" align="flex-end">
|
||||
<TextInput
|
||||
style={{ flex: 1 }}
|
||||
label="Custom Hex Code"
|
||||
placeholder="#FF0000"
|
||||
value={customHex}
|
||||
onChange={handleHexInputChange}
|
||||
error={customHex && !isValidHex ? 'Invalid hex color format' : undefined}
|
||||
w="100%"
|
||||
/>
|
||||
|
||||
{isValidHex && (
|
||||
<ColorSwatch
|
||||
color={customHex}
|
||||
size={36}
|
||||
style={{ marginBottom: customHex && !isValidHex ? 24 : 0 }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamColorPicker;
|
||||
@@ -1,81 +1,88 @@
|
||||
import { FileInput, Stack, TextInput, Textarea } from "@mantine/core";
|
||||
import { Badge, FileInput, Group, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useForm, UseFormInput } from "@mantine/form";
|
||||
import { LinkIcon } from "@phosphor-icons/react";
|
||||
import SlidePanel, { SlidePanelField } from "@/components/sheet/slide-panel";
|
||||
import { TournamentInput } from "@/features/tournaments/types";
|
||||
import { isNotEmpty } from "@mantine/form";
|
||||
import useCreateTournament from "../hooks/use-create-team";
|
||||
import useUpdateTournament from "../hooks/use-update-tournament";
|
||||
import useCreateTeam from "../../hooks/use-create-team";
|
||||
import useUpdateTeam from "../../hooks/use-update-team";
|
||||
import toast from "@/lib/sonner";
|
||||
import { logger } from "..";
|
||||
import { logger } from "../..";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { tournamentKeys } from "@/features/tournaments/queries";
|
||||
import { useCallback } from "react";
|
||||
import { DateTimePicker } from "@/components/date-time-picker";
|
||||
import { TeamInput } from "../../types";
|
||||
import { teamKeys } from "../../queries";
|
||||
import SongPicker from "./song-picker";
|
||||
import TeamColorPicker from "./color-picker";
|
||||
import PlayersPicker from "./players-picker";
|
||||
|
||||
interface TournamentFormProps {
|
||||
interface TeamFormProps {
|
||||
close: () => void;
|
||||
initialValues?: Partial<TournamentInput>;
|
||||
initialValues?: Partial<TeamInput>;
|
||||
teamId?: string;
|
||||
tournamentId?: string;
|
||||
}
|
||||
|
||||
const TournamentForm = ({
|
||||
const TeamForm = ({
|
||||
close,
|
||||
initialValues,
|
||||
teamId,
|
||||
tournamentId,
|
||||
}: TournamentFormProps) => {
|
||||
const isEditMode = !!tournamentId;
|
||||
}: TeamFormProps) => {
|
||||
const isEditMode = !!teamId;
|
||||
|
||||
const config: UseFormInput<TournamentInput> = {
|
||||
const config: UseFormInput<TeamInput> = {
|
||||
initialValues: {
|
||||
name: initialValues?.name || "",
|
||||
location: initialValues?.location || "",
|
||||
desc: initialValues?.desc || "",
|
||||
start_time: initialValues?.start_time || "",
|
||||
enroll_time: initialValues?.enroll_time || "",
|
||||
end_time: initialValues?.end_time || "",
|
||||
primary_color: initialValues?.primary_color,
|
||||
accent_color: initialValues?.accent_color,
|
||||
song_id: initialValues?.song_id,
|
||||
song_name: initialValues?.song_name,
|
||||
song_artist: initialValues?.song_artist,
|
||||
song_album: initialValues?.song_album,
|
||||
song_year: initialValues?.song_year,
|
||||
song_start: initialValues?.song_start,
|
||||
song_end: initialValues?.song_end,
|
||||
song_image_url: initialValues?.song_image_url,
|
||||
logo: undefined,
|
||||
players: initialValues?.players || []
|
||||
},
|
||||
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"),
|
||||
players: (value: string[]) => value.length > 1 && value[1] !== '' ? undefined : "Players are required"
|
||||
},
|
||||
};
|
||||
|
||||
const form = useForm(config);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: createTournament, isPending: createPending } =
|
||||
useCreateTournament();
|
||||
const { mutate: updateTournament, isPending: updatePending } =
|
||||
useUpdateTournament(tournamentId || "");
|
||||
const { mutate: createTournament, isPending: createPending } = useCreateTeam();
|
||||
const { mutate: updateTournament, isPending: updatePending } = useUpdateTeam(teamId!);
|
||||
|
||||
const isPending = createPending || updatePending;
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (values: TournamentInput) => {
|
||||
const { logo, ...tournamentData } = values;
|
||||
async (values: TeamInput) => {
|
||||
const { logo, ...teamData } = values;
|
||||
|
||||
const mutation = isEditMode ? updateTournament : createTournament;
|
||||
const successMessage = isEditMode
|
||||
? "Tournament updated successfully!"
|
||||
: "Tournament created successfully!";
|
||||
? "Team updated successfully!"
|
||||
: "Team created successfully!";
|
||||
const errorMessage = isEditMode
|
||||
? "Failed to update tournament"
|
||||
: "Failed to create tournament";
|
||||
? "Failed to update team"
|
||||
: "Failed to create team";
|
||||
|
||||
mutation(tournamentData, {
|
||||
onSuccess: async (tournament) => {
|
||||
if (logo && tournament) {
|
||||
mutation(teamData, {
|
||||
onSuccess: async (team) => {
|
||||
if (logo && team) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("tournamentId", tournament.id);
|
||||
formData.append("teamId", team.id);
|
||||
formData.append("logo", logo);
|
||||
|
||||
const response = await fetch("/api/tournaments/upload-logo", {
|
||||
const response = await fetch("/api/teams/upload-logo", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
@@ -87,22 +94,22 @@ const TournamentForm = ({
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: tournamentKeys.list });
|
||||
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tournamentKeys.details(result.tournament!.id),
|
||||
queryKey: teamKeys.details(result.team!.id),
|
||||
});
|
||||
queryClient.setQueryData(
|
||||
tournamentKeys.details(result.tournament!.id),
|
||||
result.tournament
|
||||
tournamentKeys.details(result.team!.id),
|
||||
result.team
|
||||
);
|
||||
|
||||
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}`;
|
||||
? `Team updated but logo upload failed: ${error.message}`
|
||||
: `Team created but logo upload failed: ${error.message}`;
|
||||
toast.error(logoErrorMessage);
|
||||
logger.error("Tournament logo upload error", error);
|
||||
logger.error("Team logo upload error", error);
|
||||
}
|
||||
} else {
|
||||
toast.success(successMessage);
|
||||
@@ -112,7 +119,7 @@ const TournamentForm = ({
|
||||
onError: (error: any) => {
|
||||
toast.error(`${errorMessage}: ${error.message}`);
|
||||
logger.error(
|
||||
`Tournament ${isEditMode ? "update" : "create"} error`,
|
||||
`Team ${isEditMode ? "update" : "create"} error`,
|
||||
error
|
||||
);
|
||||
},
|
||||
@@ -123,9 +130,9 @@ const TournamentForm = ({
|
||||
|
||||
return (
|
||||
<SlidePanel
|
||||
onSubmit={form.onSubmit(handleSubmit)}
|
||||
onSubmit={form.onSubmit((values) => console.log(values))}
|
||||
onCancel={close}
|
||||
submitText={isEditMode ? "Update Tournament" : "Create Tournament"}
|
||||
submitText={isEditMode ? "Update Team" : "Create Team"}
|
||||
cancelText="Cancel"
|
||||
loading={isPending}
|
||||
>
|
||||
@@ -136,19 +143,6 @@ const TournamentForm = ({
|
||||
key={form.key("name")}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
label="Location"
|
||||
withAsterisk
|
||||
key={form.key("location")}
|
||||
{...form.getInputProps("location")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Description"
|
||||
key={form.key("desc")}
|
||||
{...form.getInputProps("desc")}
|
||||
minRows={3}
|
||||
/>
|
||||
|
||||
<FileInput
|
||||
key={form.key("logo")}
|
||||
@@ -158,6 +152,50 @@ const TournamentForm = ({
|
||||
{...form.getInputProps("logo")}
|
||||
/>
|
||||
|
||||
{
|
||||
tournamentId && (
|
||||
<PlayersPicker
|
||||
tournamentId={tournamentId}
|
||||
key={form.key("players")}
|
||||
{...form.getInputProps("players")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<SlidePanelField
|
||||
key={form.key("primary_color")}
|
||||
{...form.getInputProps("primary_color")}
|
||||
Component={TeamColorPicker}
|
||||
title="Select Primary Color"
|
||||
placeholder="Select Primary Color"
|
||||
label="Primary Color"
|
||||
formatValue={(value) => (
|
||||
<Group>
|
||||
<Badge variant="filled" radius="sm" color={value} />
|
||||
{value}
|
||||
</Group>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SlidePanelField
|
||||
key={form.key("accent_color")}
|
||||
{...form.getInputProps("accent_color")}
|
||||
Component={TeamColorPicker}
|
||||
title="Select Accent Color"
|
||||
placeholder="Select Accent Color"
|
||||
label="Accent Color"
|
||||
formatValue={(value) => (
|
||||
<Group>
|
||||
<Badge variant="filled" radius="sm" color={value} />
|
||||
{value}
|
||||
</Group>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SongPicker form={form} />
|
||||
|
||||
|
||||
{/*
|
||||
<SlidePanelField
|
||||
key={form.key("start_time")}
|
||||
{...form.getInputProps("start_time")}
|
||||
@@ -221,9 +259,10 @@ const TournamentForm = ({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
*/}
|
||||
</Stack>
|
||||
</SlidePanel>
|
||||
);
|
||||
};
|
||||
|
||||
export default TournamentForm;
|
||||
export default TeamForm;
|
||||
62
src/features/teams/components/team-form/players-picker.tsx
Normal file
62
src/features/teams/components/team-form/players-picker.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SlidePanelField } from "@/components/sheet/slide-panel";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useUnenrolledPlayers } from "@/features/players/queries";
|
||||
import { Autocomplete, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
interface PlayersPickerProps {
|
||||
tournamentId: string;
|
||||
value?: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
error?: string
|
||||
}
|
||||
|
||||
const PlayersPicker = ({ tournamentId, value = [], onChange, disabled, error }: PlayersPickerProps) => {
|
||||
const { roles, user } = useAuth();
|
||||
const isAdmin = roles.includes("Admin");
|
||||
|
||||
const { data } = useUnenrolledPlayers(tournamentId);
|
||||
|
||||
const options = useMemo(() => data.filter(p => p.id != user?.id).map(p => (p.first_name + " " + p.last_name)), [data])
|
||||
|
||||
const findPlayerId = useCallback((name?: string) => {
|
||||
const player = data.find(player => player?.first_name + " " + player?.last_name === name)
|
||||
return player?.id || ""
|
||||
}, [data])
|
||||
|
||||
const findPlayerName = useCallback((id?: string) => {
|
||||
const player = data.find(player => player.id === id)
|
||||
return player?.first_name + " " + player?.last_name
|
||||
}, [data])
|
||||
|
||||
const [playerOne, setPlayerOne] = useState(value.length ? findPlayerName(value[0]) : findPlayerName(user!.id));
|
||||
const [playerTwo, setPlayerTwo] = useState(value.length > 1 ? findPlayerName(value[1]) : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
onChange([findPlayerId(playerOne), findPlayerId(playerTwo)])
|
||||
}, [playerOne, playerTwo])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Autocomplete
|
||||
label="Player 1"
|
||||
data={options}
|
||||
disabled={!isAdmin || disabled}
|
||||
value={playerOne}
|
||||
onChange={setPlayerOne}
|
||||
/>
|
||||
<Autocomplete
|
||||
label="Player 2"
|
||||
data={options}
|
||||
disabled={disabled && !isAdmin}
|
||||
value={playerTwo}
|
||||
onChange={setPlayerTwo}
|
||||
withAsterisk
|
||||
error={error}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayersPicker;
|
||||
56
src/features/teams/components/team-form/song-picker.tsx
Normal file
56
src/features/teams/components/team-form/song-picker.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SlidePanelField } from "@/components/sheet/slide-panel";
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { TeamInput } from "../../types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Song {
|
||||
song_id: string;
|
||||
song_name: string;
|
||||
song_artist: string;
|
||||
song_album: string;
|
||||
song_year?: number;
|
||||
song_start?: number;
|
||||
song_end?: number;
|
||||
song_image_url: string;
|
||||
}
|
||||
|
||||
interface SongPickerProps {
|
||||
form: UseFormReturnType<TeamInput>
|
||||
}
|
||||
|
||||
const SongPicker = ({ form }: SongPickerProps) => {
|
||||
const [song, setSong] = useState<Song>();
|
||||
|
||||
useEffect(() => {
|
||||
const values = form.getValues();
|
||||
|
||||
setSong({
|
||||
song_id: values.song_id || "",
|
||||
song_name: values.song_name || "",
|
||||
song_artist: values.song_artist || "",
|
||||
song_album: values.song_album || "",
|
||||
song_year: values.song_year,
|
||||
song_start: values.song_start,
|
||||
song_end: values.song_end,
|
||||
song_image_url: values.song_image_url || "",
|
||||
})
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SlidePanelField
|
||||
key={"song-picker"}
|
||||
value={""}
|
||||
onChange={console.log}
|
||||
Component={() => (
|
||||
<Stack>
|
||||
<Text>Song picker</Text>
|
||||
</Stack>
|
||||
)}
|
||||
title={"Select Song"}
|
||||
label={"Song"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SongPicker;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { ColorPicker, TextInput, Stack, Group, ColorSwatch, Text } from '@mantine/core';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
interface TeamColorPickerProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const TeamColorPicker: React.FC<TeamColorPickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
label = "Select Color"
|
||||
}) => {
|
||||
const [customHex, setCustomHex] = useState(value || '');
|
||||
|
||||
const isValidHex = useMemo(() => {
|
||||
const hexRegex = /^#[0-9A-F]{6}$/i;
|
||||
return hexRegex.test(customHex);
|
||||
}, [customHex]);
|
||||
|
||||
const handleColorChange = useCallback((color: string) => {
|
||||
setCustomHex(color);
|
||||
onChange(color);
|
||||
}, [onChange]);
|
||||
|
||||
const handleHexInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const hex = event.currentTarget.value;
|
||||
setCustomHex(hex);
|
||||
|
||||
if (/^#[0-9A-F]{6}$/i.test(hex)) {
|
||||
onChange(hex);
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
const presetColors = [
|
||||
'#FF0000', '#00FF00', '#0000FF', '#FFFF00',
|
||||
'#FF00FF', '#00FFFF', '#FFA500', '#800080',
|
||||
'#008000', '#000080', '#800000', '#808000'
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="md" p="md">
|
||||
<Text fw={500}>{label}</Text>
|
||||
|
||||
<ColorPicker
|
||||
value={value || '#000000'}
|
||||
onChange={handleColorChange}
|
||||
format="hex"
|
||||
swatches={presetColors}
|
||||
withPicker={true}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Group gap="xs" align="flex-end">
|
||||
<TextInput
|
||||
style={{ flex: 1 }}
|
||||
label="Custom Hex Code"
|
||||
placeholder="#FF0000"
|
||||
value={customHex}
|
||||
onChange={handleHexInputChange}
|
||||
error={customHex && !isValidHex ? 'Invalid hex color format' : undefined}
|
||||
/>
|
||||
|
||||
{isValidHex && (
|
||||
<ColorSwatch
|
||||
color={customHex}
|
||||
size={36}
|
||||
style={{ marginBottom: customHex && !isValidHex ? 24 : 0 }}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamColorPicker;
|
||||
@@ -3,7 +3,8 @@ import Header from "./header";
|
||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||
import TournamentList from "@/features/tournaments/components/tournament-list";
|
||||
import StatsOverview from "@/shared/components/stats-overview";
|
||||
import { useTeam, useTeamStats } from "../../queries";
|
||||
import { useTeam, useTeamMatches, useTeamStats } from "../../queries";
|
||||
import MatchList from "@/features/matches/components/match-list";
|
||||
|
||||
interface ProfileProps {
|
||||
id: string;
|
||||
@@ -11,6 +12,7 @@ interface ProfileProps {
|
||||
|
||||
const TeamProfile = ({ id }: ProfileProps) => {
|
||||
const { data: team } = useTeam(id);
|
||||
const { data: matches } = useTeamMatches(id);
|
||||
const { data: stats, isLoading: statsLoading, error: statsError } = useTeamStats(id);
|
||||
if (!team) return <Text p="md">Team not found</Text>;
|
||||
|
||||
@@ -21,7 +23,7 @@ const TeamProfile = ({ id }: ProfileProps) => {
|
||||
},
|
||||
{
|
||||
label: "Matches",
|
||||
content: <Text p="md">Matches feed will go here</Text>,
|
||||
content: <MatchList matches={matches || []} />,
|
||||
},
|
||||
{
|
||||
label: "Tournaments",
|
||||
@@ -35,7 +37,7 @@ const TeamProfile = ({ id }: ProfileProps) => {
|
||||
return (
|
||||
<>
|
||||
<Header name={team.name} logo={team.logo} />
|
||||
<Box m="sm" mt="lg">
|
||||
<Box m="xs" mt="lg">
|
||||
<SwipeableTabs tabs={tabs} />
|
||||
</Box>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user