sendou.ink/components/u/BuildModal.tsx
2021-03-08 17:48:05 +02:00

402 lines
13 KiB
TypeScript

import {
Box,
Button,
Checkbox,
CheckboxGroup,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Textarea,
useToast,
} from "@chakra-ui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { t, Trans } from "@lingui/macro";
import { Ability } from "@prisma/client";
import ViewSlots from "components/builds/ViewSlots";
import GearSelector from "components/common/GearSelector";
import WeaponSelector from "components/common/WeaponSelector";
import { useUser } from "hooks/common";
import { GetBuildsByUserData } from "prisma/queries/getBuildsByUser";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { FiTrash } from "react-icons/fi";
import { mutate } from "swr";
import { getToastOptions } from "utils/getToastOptions";
import { weapons } from "utils/lists/weapons";
import { sendData } from "utils/postData";
import { Unpacked } from "utils/types";
import {
buildSchema,
DESCRIPTION_CHARACTER_LIMIT,
TITLE_CHARACTER_LIMIT,
} from "utils/validators/build";
import * as z from "zod";
import AbilitiesSelector from "./AbilitiesSelector";
interface Props {
onClose: () => void;
build?: Unpacked<NonNullable<GetBuildsByUserData>>;
weaponFromQuery?: string;
}
type FormData = z.infer<typeof buildSchema>;
const BuildModal: React.FC<Props> = ({ onClose, build, weaponFromQuery }) => {
const [sending, setSending] = useState(false);
const [deleting, setDeleting] = useState(false);
const [loggedInUser] = useUser();
const { handleSubmit, errors, register, watch, control } = useForm<FormData>({
resolver: zodResolver(buildSchema),
defaultValues: {
headAbilities: ([
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
] as unknown) as Ability[],
clothingAbilities: ([
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
] as unknown) as Ability[],
shoesAbilities: ([
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
"UNKNOWN",
] as unknown) as Ability[],
weapon: weapons.includes(weaponFromQuery as any)
? weaponFromQuery
: undefined,
...build,
},
});
const watchTitle = watch("title", build?.title);
const watchDescription = watch("description", build?.description);
const toast = useToast();
const onSubmit = async (formData: FormData) => {
if (!loggedInUser) {
console.error("Unexpected no logged in user");
return;
}
setSending(true);
const mutationData = { ...formData };
for (const [key, value] of Object.entries(mutationData)) {
if (value === "" || value === undefined) {
// @ts-ignore
mutationData[key] = null;
}
}
const success = await sendData(
build ? "PUT" : "POST",
"/api/builds",
build ? { ...mutationData, id: build.id } : mutationData
);
setSending(false);
if (!success) return;
mutate(`/api/users/${loggedInUser.id}/builds`);
toast(
getToastOptions(build ? t`Build updated` : t`New build added`, "success")
);
onClose();
};
const onDelete = async () => {
if (!loggedInUser) {
console.error("Unexpected no logged in user");
return;
}
if (!build) {
console.error("Unexpected no build");
return;
}
setDeleting(true);
const success = await sendData("DELETE", "/api/builds", { id: build.id });
setDeleting(false);
if (!success) return;
mutate(`/api/users/${loggedInUser.id}/builds`);
toast(getToastOptions(t`Build deleted`, "success"));
onClose();
};
return (
<Modal isOpen onClose={onClose} size="xl" closeOnOverlayClick={false}>
<ModalOverlay>
<ModalContent>
<ModalHeader>
{build ? (
<Trans>Editing build</Trans>
) : (
<Trans>Adding a new build</Trans>
)}
</ModalHeader>
<ModalCloseButton borderRadius="50%" />
<form onSubmit={handleSubmit(onSubmit)}>
<ModalBody pb={6}>
{build && (
<Button
leftIcon={<FiTrash />}
variant="outline"
color="red.500"
mb={6}
isLoading={deleting}
onClick={async () => {
if (window.confirm(t`Delete the build?`)) await onDelete();
}}
>
<Trans>Delete build</Trans>
</Button>
)}
<FormLabel htmlFor="weapon">
<Trans>Weapon</Trans>
</FormLabel>
<FormControl isInvalid={!!errors.weapon}>
<Controller
name="weapon"
control={control}
defaultValue={null}
render={({ onChange, value }) => (
<WeaponSelector
value={value}
setValue={onChange}
pool="WITH_ALTS"
isMulti={false}
/>
)}
/>
<FormErrorMessage>
{t`Choose a weapon for your build`}
</FormErrorMessage>
</FormControl>
{/* yeah....... didn't find an easier way to do this with the library so here we are */}
<FormControl
isInvalid={
!!errors.headAbilities ||
!!errors.clothingAbilities ||
!!errors.shoesAbilities
}
>
<Controller
name="headAbilities"
control={control}
render={({
onChange: onHeadChange,
value: headAbilities,
}) => (
<Controller
name="clothingAbilities"
control={control}
render={({
onChange: onClothingChange,
value: clothingAbilities,
}) => (
<Controller
name="shoesAbilities"
control={control}
render={({
onChange: onShoesChange,
value: shoesAbilities,
}) => (
<Box mt={4}>
<ViewSlots
abilities={{
headAbilities,
clothingAbilities,
shoesAbilities,
}}
onAbilityClick={(gear, index) => {
const abilityArrays = {
headAbilities,
clothingAbilities,
shoesAbilities,
};
const onChange = {
headAbilities: onHeadChange,
clothingAbilities: onClothingChange,
shoesAbilities: onShoesChange,
};
const newAbilityArray = [
...abilityArrays[gear],
];
newAbilityArray[index] = "UNKNOWN";
onChange[gear](newAbilityArray);
}}
/>
<AbilitiesSelector
abilities={{
headAbilities,
clothingAbilities,
shoesAbilities,
}}
setAbilities={(newAbilities) => {
onHeadChange(newAbilities.headAbilities);
onClothingChange(
newAbilities.clothingAbilities
);
onShoesChange(newAbilities.shoesAbilities);
}}
/>
</Box>
)}
/>
)}
/>
)}
/>
<FormErrorMessage mx="auto">{t`Your build is missing some abilities`}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.title}>
<FormLabel htmlFor="title" mt={4}>
<Trans>Title</Trans>
</FormLabel>
<Input name="title" ref={register} />
<FormHelperText>
{(watchTitle ?? "").length}/{TITLE_CHARACTER_LIMIT}
</FormHelperText>
<FormErrorMessage>{errors.title?.message}</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.description}>
<FormLabel htmlFor="description" mt={4}>
<Trans>Description</Trans>
</FormLabel>
<Textarea name="description" ref={register} />
<FormHelperText>
{(watchDescription ?? "").length}/
{DESCRIPTION_CHARACTER_LIMIT}
</FormHelperText>
<FormErrorMessage>
{errors.description?.message}
</FormErrorMessage>
</FormControl>
<FormLabel htmlFor="headGear" mt={4}>
<Trans>Head</Trans>
</FormLabel>
<Controller
name="headGear"
control={control}
defaultValue=""
render={({ onChange, value, name }) => (
<GearSelector slot="head" value={value} setValue={onChange} />
)}
/>
<FormLabel htmlFor="clothingGear" mt={4}>
<Trans>Clothing</Trans>
</FormLabel>
<Controller
name="clothingGear"
control={control}
defaultValue=""
render={({ onChange, value }) => (
<GearSelector
slot="clothing"
value={value}
setValue={onChange}
/>
)}
/>
<FormLabel htmlFor="shoesGear" mt={4}>
<Trans>Shoes</Trans>
</FormLabel>
<Controller
name="shoesGear"
control={control}
defaultValue=""
render={({ onChange, value }) => (
<GearSelector
slot="shoes"
value={value}
setValue={onChange}
/>
)}
/>
<FormControl isInvalid={!!errors.modes}>
<FormLabel htmlFor="modes" mt={4}>
<Trans>Modes</Trans>
</FormLabel>
<Controller
name="modes"
control={control}
defaultValue={[]}
render={({ onChange, value }) => (
<CheckboxGroup value={value} onChange={onChange}>
<Stack spacing={4} direction="row">
<Checkbox value="TW">
<Trans>TW</Trans>
</Checkbox>
<Checkbox value="SZ">
<Trans>SZ</Trans>
</Checkbox>
<Checkbox value="TC">
<Trans>TC</Trans>
</Checkbox>
<Checkbox value="RM">
<Trans>RM</Trans>
</Checkbox>
<Checkbox value="CB">
<Trans>CB</Trans>
</Checkbox>
</Stack>
</CheckboxGroup>
)}
/>
<FormHelperText>
<Trans>
Choose at least one mode where you use this build
</Trans>
</FormHelperText>
<FormErrorMessage>
{/* @ts-ignore */}
{t`Select at least one mode`}
</FormErrorMessage>
</FormControl>
</ModalBody>
<ModalFooter>
<Button mr={3} type="submit" isLoading={sending}>
<Trans>Save</Trans>
</Button>
<Button onClick={onClose} variant="outline">
<Trans>Cancel</Trans>
</Button>
</ModalFooter>
</form>
</ModalContent>
</ModalOverlay>
</Modal>
);
};
export default BuildModal;