Files
flxn-app/src/features/teams/components/team-form/index.tsx

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;