sendou.ink/app/features/tournament-organization/routes/org.$slug.edit.tsx
2025-12-29 19:21:11 +02:00

266 lines
7.0 KiB
TypeScript

import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useLoaderData } from "react-router";
import type { z } from "zod/v4";
import { FormMessage } from "~/components/FormMessage";
import { AddFieldButton } from "~/components/form/AddFieldButton";
import { FormFieldset } from "~/components/form/FormFieldset";
import { InputFormField } from "~/components/form/InputFormField";
import { SelectFormField } from "~/components/form/SelectFormField";
import { SendouForm } from "~/components/form/SendouForm";
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
import { TextArrayFormField } from "~/components/form/TextArrayFormField";
import { ToggleFormField } from "~/components/form/ToggleFormField";
import { UserSearchFormField } from "~/components/form/UserSearchFormField";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
import { TOURNAMENT_ORGANIZATION_ROLES } from "~/db/tables";
import { BadgesSelector } from "~/features/badges/components/BadgesSelector";
import { wrapToValueStringArrayWithDefault } from "~/utils/form";
import type { Unpacked } from "~/utils/types";
import { uploadImagePage } from "~/utils/urls";
import { action } from "../actions/org.$slug.edit.server";
import { loader } from "../loaders/org.$slug.edit.server";
import { handle, meta } from "../routes/org.$slug";
import { TOURNAMENT_ORGANIZATION } from "../tournament-organization-constants";
import { organizationEditSchema } from "../tournament-organization-schemas";
export { action, handle, loader, meta };
type FormFields = z.infer<typeof organizationEditSchema> & {
members: Array<
Omit<
Unpacked<z.infer<typeof organizationEditSchema>["members"]>,
"userId"
> & {
userId: number | null;
}
>;
};
export default function TournamentOrganizationEditPage() {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["org", "common"]);
return (
<Main>
<SendouForm
heading={t("org:edit.form.title")}
schema={organizationEditSchema}
defaultValues={{
name: data.organization.name,
description: data.organization.description,
socials: wrapToValueStringArrayWithDefault(data.organization.socials),
members: data.organization.members.map((member) => ({
userId: member.id,
role: member.role,
roleDisplayName: member.roleDisplayName,
})),
series: data.organization.series.map((series) => ({
name: series.name,
description: series.description,
showLeaderboard: Boolean(series.showLeaderboard),
})),
badges: data.organization.badges.map((badge) => badge.id),
}}
>
<Link
to={uploadImagePage({
type: "org-pfp",
slug: data.organization.slug,
})}
className="text-sm font-bold"
>
{t("org:edit.form.uploadLogo")}
</Link>
<InputFormField<FormFields>
label={t("common:forms.name")}
name="name"
/>
<TextAreaFormField<FormFields>
label={t("common:forms.description")}
name="description"
maxLength={TOURNAMENT_ORGANIZATION.DESCRIPTION_MAX_LENGTH}
/>
<MembersFormField />
<TextArrayFormField<FormFields>
label={t("org:edit.form.socialLinks.title")}
name="socials"
format="object"
/>
<SeriesFormField />
<BadgesFormField />
</SendouForm>
</Main>
);
}
function MembersFormField() {
const {
formState: { errors },
} = useFormContext<FormFields>();
const { fields, append, remove } = useFieldArray<FormFields>({
name: "members",
});
const { t } = useTranslation(["org"]);
const rootError = errors.members?.root;
return (
<div>
<Label>{t("org:edit.form.members.title")}</Label>
<div className="stack md">
{fields.map((field, i) => {
return <MemberFieldset key={field.id} idx={i} remove={remove} />;
})}
<AddFieldButton
onClick={() => {
append({ role: "MEMBER", roleDisplayName: null, userId: null });
}}
/>
{rootError && (
<FormMessage type="error">{rootError.message as string}</FormMessage>
)}
<FormMessage type="info">{t("org:edit.form.members.info")}</FormMessage>
</div>
</div>
);
}
function MemberFieldset({
idx,
remove,
}: {
idx: number;
remove: (idx: number) => void;
}) {
const { t } = useTranslation(["org"]);
const { clearErrors } = useFormContext<FormFields>();
return (
<FormFieldset
title={`#${idx + 1}`}
onRemove={() => {
remove(idx);
clearErrors("members");
}}
>
<UserSearchFormField<FormFields>
label={t("org:edit.form.members.user.title")}
name={`members.${idx}.userId` as const}
/>
<SelectFormField<FormFields>
label={t("org:edit.form.members.role.title")}
name={`members.${idx}.role` as const}
values={TOURNAMENT_ORGANIZATION_ROLES.map((role) => ({
value: role,
label: t(`org:roles.${role}`),
}))}
/>
<InputFormField<FormFields>
label={t("org:edit.form.members.roleDisplayName.title")}
name={`members.${idx}.roleDisplayName` as const}
/>
</FormFieldset>
);
}
function SeriesFormField() {
const {
formState: { errors },
} = useFormContext<FormFields>();
const { fields, append, remove } = useFieldArray<FormFields>({
name: "series",
});
const { t } = useTranslation(["org"]);
const rootError = errors.series?.root;
return (
<div>
<Label>{t("org:edit.form.series.title")}</Label>
<div className="stack md">
{fields.map((field, i) => {
return <SeriesFieldset key={field.id} idx={i} remove={remove} />;
})}
<AddFieldButton
onClick={() => {
append({ description: "", name: "", showLeaderboard: false });
}}
/>
{rootError && (
<FormMessage type="error">{rootError.message as string}</FormMessage>
)}
</div>
</div>
);
}
function SeriesFieldset({
idx,
remove,
}: {
idx: number;
remove: (idx: number) => void;
}) {
const { t } = useTranslation(["org", "common"]);
const { clearErrors } = useFormContext<FormFields>();
return (
<FormFieldset
title={`#${idx + 1}`}
onRemove={() => {
remove(idx);
clearErrors("series");
}}
>
<InputFormField<FormFields>
label={t("org:edit.form.series.seriesName.title")}
name={`series.${idx}.name` as const}
/>
<TextAreaFormField<FormFields>
label={t("common:forms.description")}
name={`series.${idx}.description` as const}
maxLength={TOURNAMENT_ORGANIZATION.DESCRIPTION_MAX_LENGTH}
/>
<ToggleFormField<FormFields>
label={t("org:edit.form.series.showLeaderboard.title")}
name={`series.${idx}.showLeaderboard` as const}
/>
</FormFieldset>
);
}
function BadgesFormField() {
const { t } = useTranslation(["org"]);
const methods = useFormContext<FormFields>();
const data = useLoaderData<typeof loader>();
return (
<div>
<Label>{t("org:edit.form.badges.title")}</Label>
<Controller
control={methods.control}
name="badges"
render={({ field: { onChange, onBlur, value } }) => (
<BadgesSelector
options={data.badgeOptions}
selectedBadges={value}
onBlur={onBlur}
onChange={onChange}
/>
)}
/>
</div>
);
}