mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-26 09:20:24 -05:00
* From scrims * wip * wip * wip * wip * WIP * wip * wip * wip * wip * wip * import ordering
274 lines
7.0 KiB
TypeScript
274 lines
7.0 KiB
TypeScript
import { useLoaderData } from "@remix-run/react";
|
|
import * as React from "react";
|
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { z } from "zod";
|
|
import { Label } from "~/components/Label";
|
|
import { DateTimeFormField } from "~/components/form/DateTimeFormField";
|
|
import { MyForm } from "~/components/form/MyForm";
|
|
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
|
|
import { nullFilledArray } from "~/utils/arrays";
|
|
import type { SendouRouteHandle } from "~/utils/remix.server";
|
|
import { FormMessage } from "../../../components/FormMessage";
|
|
import { Main } from "../../../components/Main";
|
|
import { WithFormField } from "../components/WithFormField";
|
|
import { LUTI_DIVS, SCRIM } from "../scrims-constants";
|
|
import {
|
|
MAX_SCRIM_POST_TEXT_LENGTH,
|
|
scrimsNewActionSchema,
|
|
} from "../scrims-schemas";
|
|
import type { LutiDiv } from "../scrims-types";
|
|
|
|
import { action } from "../actions/scrims.new.server";
|
|
import { type ScrimsNewLoaderData, loader } from "../loaders/scrims.new.server";
|
|
export { loader, action };
|
|
|
|
export const handle: SendouRouteHandle = {
|
|
i18n: "scrims",
|
|
};
|
|
|
|
type FormFields = z.infer<typeof scrimsNewActionSchema>;
|
|
|
|
export default function NewScrimPage() {
|
|
const { t } = useTranslation(["scrims"]);
|
|
const data = useLoaderData<typeof loader>();
|
|
|
|
return (
|
|
<Main>
|
|
<MyForm
|
|
schema={scrimsNewActionSchema}
|
|
heading={t("scrims:forms.title")}
|
|
defaultValues={{
|
|
postText: "",
|
|
at: new Date(),
|
|
divs: null,
|
|
baseVisibility: "PUBLIC",
|
|
notFoundVisibility: {
|
|
at: null,
|
|
forAssociation: "PUBLIC",
|
|
},
|
|
from:
|
|
data.teams.length > 0
|
|
? { mode: "TEAM", teamId: data.teams[0].id }
|
|
: {
|
|
mode: "PICKUP",
|
|
users: nullFilledArray(
|
|
SCRIM.MAX_PICKUP_SIZE_EXCLUDING_OWNER,
|
|
) as unknown as number[],
|
|
},
|
|
}}
|
|
>
|
|
<WithFormField usersTeams={data.teams} />
|
|
|
|
<DateTimeFormField<FormFields>
|
|
label={t("scrims:forms.when.title")}
|
|
name="at"
|
|
bottomText={t("scrims:forms.when.explanation")}
|
|
/>
|
|
|
|
<BaseVisibilityFormField associations={data.associations} />
|
|
|
|
<NotFoundVisibilityFormField associations={data.associations} />
|
|
|
|
<LutiDivsFormField />
|
|
|
|
<TextAreaFormField<FormFields>
|
|
label={t("scrims:forms.text.title")}
|
|
name="postText"
|
|
maxLength={MAX_SCRIM_POST_TEXT_LENGTH}
|
|
/>
|
|
</MyForm>
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function BaseVisibilityFormField({
|
|
associations,
|
|
}: { associations: ScrimsNewLoaderData["associations"] }) {
|
|
const { t } = useTranslation(["scrims"]);
|
|
const methods = useFormContext<FormFields>();
|
|
|
|
const error = methods.formState.errors.baseVisibility;
|
|
|
|
const noAssociations =
|
|
associations.virtual.length === 0 && associations.actual.length === 0;
|
|
|
|
return (
|
|
<div>
|
|
<Label htmlFor="visibility">{t("scrims:forms.visibility.title")}</Label>
|
|
{noAssociations ? (
|
|
<FormMessage type="info">
|
|
{t("scrims:forms.visibility.noneAvailable")}
|
|
</FormMessage>
|
|
) : (
|
|
<AssociationSelect
|
|
associations={associations}
|
|
id="visibility"
|
|
{...methods.register("baseVisibility")}
|
|
/>
|
|
)}
|
|
|
|
{error && (
|
|
<FormMessage type="error">{error.message as string}</FormMessage>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function NotFoundVisibilityFormField({
|
|
associations,
|
|
}: { associations: ScrimsNewLoaderData["associations"] }) {
|
|
const { t } = useTranslation(["scrims"]);
|
|
const date = useWatch<FormFields>({ name: "notFoundVisibility.at" }) ?? "";
|
|
const methods = useFormContext<FormFields>();
|
|
|
|
const error = methods.formState.errors.notFoundVisibility;
|
|
|
|
const noAssociations =
|
|
associations.virtual.length === 0 && associations.actual.length === 0;
|
|
|
|
if (noAssociations) return null;
|
|
|
|
return (
|
|
<div>
|
|
<div className="stack horizontal sm">
|
|
<DateTimeFormField<FormFields>
|
|
label={t("scrims:forms.notFoundVisibility.title")}
|
|
name="notFoundVisibility.at"
|
|
/>
|
|
{date ? (
|
|
<div>
|
|
<Label htmlFor="not-found-visibility">
|
|
{t("scrims:forms.visibility.title")}
|
|
</Label>
|
|
<AssociationSelect
|
|
associations={associations}
|
|
id="not-found-visibility"
|
|
{...methods.register("notFoundVisibility.forAssociation")}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{error ? (
|
|
<FormMessage type="error">{error.message as string}</FormMessage>
|
|
) : (
|
|
<FormMessage type="info">
|
|
{t("scrims:forms.notFoundVisibility.explanation")}
|
|
</FormMessage>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const AssociationSelect = React.forwardRef<
|
|
HTMLSelectElement,
|
|
{
|
|
associations: ScrimsNewLoaderData["associations"];
|
|
} & React.SelectHTMLAttributes<HTMLSelectElement>
|
|
>(({ associations, ...rest }, ref) => {
|
|
const { t } = useTranslation(["scrims"]);
|
|
|
|
return (
|
|
<select ref={ref} {...rest}>
|
|
<option value="PUBLIC">{t("scrims:forms.visibility.public")}</option>
|
|
{associations.virtual.map((association) => (
|
|
<option key={association} value={association}>
|
|
{association}
|
|
</option>
|
|
))}
|
|
{associations.actual.map((association) => (
|
|
<option key={association.id} value={association.id}>
|
|
{association.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
);
|
|
});
|
|
|
|
function LutiDivsFormField() {
|
|
const methods = useFormContext<FormFields>();
|
|
|
|
const error = methods.formState.errors.divs;
|
|
|
|
return (
|
|
<div>
|
|
<Controller
|
|
control={methods.control}
|
|
name="divs"
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<LutiDivsSelector value={value} onChange={onChange} onBlur={onBlur} />
|
|
)}
|
|
/>
|
|
|
|
{error && (
|
|
<FormMessage type="error">{error.message as string}</FormMessage>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type LutiDivEdit = {
|
|
max: LutiDiv | null;
|
|
min: LutiDiv | null;
|
|
};
|
|
|
|
function LutiDivsSelector({
|
|
value,
|
|
onChange,
|
|
onBlur,
|
|
}: {
|
|
value: LutiDivEdit | null;
|
|
onChange: (value: LutiDivEdit | null) => void;
|
|
onBlur: () => void;
|
|
}) {
|
|
const { t } = useTranslation(["scrims"]);
|
|
|
|
const onChangeMin = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv);
|
|
|
|
onChange(
|
|
newValue || value?.max
|
|
? { min: newValue, max: value?.max ?? null }
|
|
: null,
|
|
);
|
|
};
|
|
|
|
const onChangeMax = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const newValue = e.target.value === "" ? null : (e.target.value as LutiDiv);
|
|
|
|
onChange(
|
|
newValue || value?.min
|
|
? { max: newValue, min: value?.min ?? null }
|
|
: null,
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="stack horizontal sm">
|
|
<div>
|
|
<Label htmlFor="min-div">{t("scrims:forms.divs.minDiv.title")}</Label>
|
|
<select id="min-div" onChange={onChangeMin} onBlur={onBlur}>
|
|
<option value="">—</option>
|
|
{LUTI_DIVS.map((div) => (
|
|
<option key={div} value={div}>
|
|
{div}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="max-div">{t("scrims:forms.divs.maxDiv.title")}</Label>
|
|
<select id="max-div" onChange={onChangeMax} onBlur={onBlur}>
|
|
<option value="">—</option>
|
|
{LUTI_DIVS.map((div) => (
|
|
<option key={div} value={div}>
|
|
{div}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|