mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-04 16:55:03 -05:00
402 lines
13 KiB
TypeScript
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;
|