260 lines
8.0 KiB
TypeScript
260 lines
8.0 KiB
TypeScript
import { FileInput, Stack, TextInput } from "@mantine/core";
|
|
import { useForm, UseFormInput } from "@mantine/form";
|
|
import { LinkIcon } from "@phosphor-icons/react";
|
|
import SlidePanel from "@/components/sheet/slide-panel";
|
|
import { isNotEmpty } from "@mantine/form";
|
|
import useCreateTeam from "../../hooks/use-create-team";
|
|
import useUpdateTeam from "../../hooks/use-update-team";
|
|
import toast from "@/lib/sonner";
|
|
import { logger } from "../..";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { tournamentKeys } from "@/features/tournaments/queries";
|
|
import { useCallback } from "react";
|
|
import { TeamInput } from "../../types";
|
|
import { teamKeys } from "../../queries";
|
|
import SongPicker from "./song-picker";
|
|
import PlayersPicker from "./players-picker";
|
|
import imageCompression from "browser-image-compression";
|
|
|
|
interface TeamFormProps {
|
|
close: () => void;
|
|
initialValues?: Partial<TeamInput>;
|
|
teamId?: string;
|
|
tournamentId?: string;
|
|
onSubmit?: (teamId: string) => void;
|
|
}
|
|
|
|
const TeamForm = ({
|
|
close,
|
|
initialValues,
|
|
teamId,
|
|
tournamentId,
|
|
onSubmit
|
|
}: TeamFormProps) => {
|
|
const isEditMode = !!teamId;
|
|
|
|
const config: UseFormInput<TeamInput> = {
|
|
initialValues: {
|
|
name: initialValues?.name || "",
|
|
// 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_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"),
|
|
players: (value: string[]) =>
|
|
value.length > 1 && value[1] !== ""
|
|
? undefined
|
|
: "Players are required",
|
|
song_name: isNotEmpty("Song is required"),
|
|
song_start: (value, values) => {
|
|
if (values.song_name && values.song_id) {
|
|
if (value === undefined || value === null) {
|
|
return "Song start time is required";
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
song_end: (value, values) => {
|
|
if (values.song_name && values.song_id) {
|
|
if (value === undefined || value === null) {
|
|
return "Song end time is required";
|
|
}
|
|
if (values.song_start !== undefined && value !== undefined) {
|
|
const duration = value - values.song_start;
|
|
if (duration < 10) {
|
|
return "Song segment must be at least 10 seconds";
|
|
}
|
|
if (duration > 15) {
|
|
return "Song segment must be no more than 15 seconds";
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
},
|
|
};
|
|
|
|
const form = useForm(config);
|
|
const queryClient = useQueryClient();
|
|
|
|
const { mutate: createTeam, isPending: createPending } =
|
|
useCreateTeam();
|
|
const { mutate: updateTeam, isPending: updatePending } = useUpdateTeam(
|
|
teamId!
|
|
);
|
|
|
|
const isPending = createPending || updatePending;
|
|
|
|
const handleSubmit = useCallback(
|
|
async (values: TeamInput) => {
|
|
const { logo, ...teamData } = values;
|
|
|
|
const mutation = isEditMode ? updateTeam : createTeam;
|
|
const errorMessage = isEditMode
|
|
? "Failed to update team"
|
|
: "Failed to create team";
|
|
|
|
mutation(teamData, {
|
|
onSuccess: async (team: any) => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.list });
|
|
queryClient.invalidateQueries({
|
|
queryKey: teamKeys.details(team.id),
|
|
});
|
|
|
|
if (logo && team) {
|
|
try {
|
|
let processedLogo = logo;
|
|
|
|
if (logo.size > 500 * 1024) {
|
|
const compressionOptions = {
|
|
maxSizeMB: 0.5,
|
|
maxWidthOrHeight: 800,
|
|
useWebWorker: true,
|
|
fileType: logo.type,
|
|
};
|
|
|
|
try {
|
|
processedLogo = await imageCompression(logo, compressionOptions);
|
|
logger.info("image compressed", {
|
|
originalSize: logo.size,
|
|
compressedSize: processedLogo.size,
|
|
reduction: ((logo.size - processedLogo.size) / logo.size * 100).toFixed(1) + "%"
|
|
});
|
|
} catch (compressionError) {
|
|
logger.warn("compression failed, falling back", compressionError);
|
|
processedLogo = logo;
|
|
}
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append("teamId", team.id);
|
|
formData.append("logo", processedLogo);
|
|
|
|
const response = await fetch("/api/teams/upload-logo", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error || "Failed to upload logo");
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
queryClient.setQueryData(
|
|
tournamentKeys.details(result.team!.id),
|
|
result.team
|
|
);
|
|
} catch (error: any) {
|
|
const logoErrorMessage = isEditMode
|
|
? `Team updated but logo upload failed: ${error.message}`
|
|
: `Team created but logo upload failed: ${error.message}`;
|
|
toast.error(logoErrorMessage);
|
|
logger.error("Team logo upload error", error);
|
|
}
|
|
}
|
|
|
|
if (team && team.id) {
|
|
onSubmit?.(team.id)
|
|
}
|
|
close();
|
|
},
|
|
onError: (error: any) => {
|
|
toast.error(`${errorMessage}: ${error.message}`);
|
|
logger.error(`Team ${isEditMode ? "update" : "create"} error`, error);
|
|
},
|
|
});
|
|
},
|
|
[isEditMode, createTeam, updateTeam, queryClient]
|
|
);
|
|
|
|
return (
|
|
<SlidePanel
|
|
onSubmit={form.onSubmit(handleSubmit)}
|
|
onCancel={close}
|
|
submitText={isEditMode ? "Update Team" : "Create Team"}
|
|
cancelText="Cancel"
|
|
loading={isPending}
|
|
>
|
|
<Stack>
|
|
<TextInput
|
|
label="Name"
|
|
withAsterisk
|
|
disabled={isEditMode}
|
|
key={form.key("name")}
|
|
{...form.getInputProps("name")}
|
|
/>
|
|
|
|
<FileInput
|
|
key={form.key("logo")}
|
|
accept="image/png,image/jpeg,image/gif,image/jpg"
|
|
label={isEditMode ? "Change Logo" : "Logo"}
|
|
leftSection={<LinkIcon size={16} />}
|
|
{...form.getInputProps("logo")}
|
|
/>
|
|
|
|
{tournamentId && (
|
|
<PlayersPicker
|
|
tournamentId={tournamentId}
|
|
key={form.key("players")}
|
|
{...form.getInputProps("players")}
|
|
disabled={isEditMode}
|
|
/>
|
|
)}
|
|
|
|
<SongPicker
|
|
form={form}
|
|
error={form.errors.song_name as string}
|
|
/>
|
|
|
|
{/*
|
|
<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>
|
|
)}
|
|
/>
|
|
*/}
|
|
|
|
</Stack>
|
|
</SlidePanel>
|
|
);
|
|
};
|
|
|
|
export default TeamForm;
|