Skip to content

Commit

Permalink
Refactor create and delete character
Browse files Browse the repository at this point in the history
  • Loading branch information
nekiro committed Nov 27, 2024
1 parent 9e1e21c commit a500b63
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 335 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ This project aims to provide a fully featured, secure and fast AAC (Automatic Ac
- Prisma (mysql)
- Jest
- Chakra UI
- Yup
- Zod
- Trpc
- Typescript

## Features
Expand All @@ -42,6 +43,5 @@ This project aims to provide a fully featured, secure and fast AAC (Automatic Ac
- lost account interface
- guilds
- houses
- use typescript where possible
- db migrations
- tests
155 changes: 91 additions & 64 deletions src/pages/account/createcharacter.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,77 @@
import React, { useState } from "react";
import React from "react";
import Panel from "../../components/Panel";
import { withSessionSsr } from "../../lib/session";
import { fetchApi, FetchResult } from "../../lib/request";
import FormWrapper, { FormButton } from "../../components/FormWrapper";
import { createCharacterSchema } from "../../schemas/CreateCharacter";
import { Select, Text } from "@chakra-ui/react";
import { Select, Text, Container, VStack, Wrap } from "@chakra-ui/react";
import TextInput from "@component/TextInput";
import Button from "@component/Button";
import { FormField } from "@component/FormField";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { SubmitHandler } from "react-hook-form";
import { Vocation } from "@shared/enums/Vocation";
import { trpc } from "@util/trpc";
import { useFormFeedback } from "@hook/useFormFeedback";
import { Sex } from "@shared/enums/Sex";

const fields = [
{
type: "text",
name: "name",
placeholder: "3 to 29 characters",
label: { text: "Name" },
},
{
type: "select",
as: Select,
name: "vocation",
label: { text: "Vocation" },
const bannedSequences = ["tutor", "cancer", "suck", "sux", "fuck"];
const bannedWords = ["gm", "cm", "god"];

options: [
{ value: "1", text: "Sorcerer" },
{ value: "2", text: "Druid" },
{ value: "3", text: "Paladin" },
{ value: "4", text: "Knight" },
],
},
{
type: "select",
as: Select,
name: "sex",
label: { text: "Sex" },
options: [
{ value: "0", text: "Female" },
{ value: "1", text: "Male" },
],
},
];
// Names is valid when:
// - doesn't contains banned words
// - has minimum of 3 letters and maximum of 29 characters
// - first letter is upper case alphabet character
// - last letter is lower case alphabet character
// - doesn't have more than 3 words
// - contains only alphabet letters and spaces

const buttons: FormButton[] = [
{ type: "submit", btnColorType: "primary", value: "Submit" },
{ href: "/account", value: "Back" },
];

const initialValues = {
name: "",
vocation: "1",
sex: "0",
};
const schema = z.object({
name: z
.string()
.min(3, { message: "Field is required and must be at least 3 characters long" })
.max(29, { message: "Field must be at most 29 characters long" })
.regex(/^[aA-zZ\s]+$/, { message: "Invalid letters, words or format. Use a-Z and spaces." })
.refine(
(value) => {
const sequences = bannedSequences.filter((str) => value.split(" ").join("").toLowerCase().includes(str.toLowerCase()));
return sequences.length === 0;
},
{ message: "Contains illegal words" },
)
.refine((value) => /^[A-Z]/.test(value.charAt(0)), { message: "First letter must be an A-Z capital letter." })
.refine((value) => /[a-z]$/.test(value.charAt(value.length - 1)), { message: "Last letter must be an a-z letter." })
.refine((value) => !/\s\s+/.test(value), { message: "Name can't have more than one space in a row." })
.refine((value) => value.split(" ").length <= 3, { message: "Name can't have more than three words." })
.refine(
(value) => {
const words = value.split(" ").filter((str) => bannedWords.includes(str.toLowerCase()));
return words.length === 0;
},
{ message: "Contains illegal words" },
),
vocation: z.nativeEnum(Vocation),
sex: z.nativeEnum(Sex),
});

export default function CreateCharacter() {
const [response, setResponse] = useState<FetchResult | undefined>(undefined);
const {
register,
handleSubmit,
reset,
formState: { errors, isValid, isSubmitting },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const createCharacter = trpc.account.createCharacter.useMutation();
const { handleResponse, showResponse } = useFormFeedback();

const onSubmit = async (values: any, { resetForm }: any) => {
const response = await fetchApi("POST", "/api/account/createcharacter", {
data: {
name: values.name,
vocation: values.vocation,
sex: values.sex,
},
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, vocation, sex }) => {
handleResponse(async () => {
await createCharacter.mutateAsync({ name, vocation, sex });
showResponse("Character created.", "success");
});

setResponse(response);
resetForm();
reset();
};

return (
Expand All @@ -71,15 +80,33 @@ export default function CreateCharacter() {
Please choose a name, vocation and sex for your character. <br />
In any case the name must not violate the naming conventions stated in the Rules or your character might get deleted or name locked.
</Text>

<FormWrapper
validationSchema={createCharacterSchema}
onSubmit={onSubmit}
fields={fields}
buttons={buttons}
response={response}
initialValues={initialValues}
/>
<form onSubmit={handleSubmit(onSubmit)}>
<Container alignContent={"center"} padding={2}>
<VStack spacing={5}>
<FormField key={"name"} error={errors.name?.message} name={"name"} label={"Name"}>
<TextInput type="name" {...register("name")} />
</FormField>
<FormField key={"vocation"} error={errors.vocation?.message} name={"vocation"} label="Vocation">
<Select {...register("vocation")}>
{Object.entries(Vocation).map(([key, value]) => (
<option value={value}>{key}</option>
))}
</Select>
</FormField>
<FormField key={"sex"} error={errors.sex?.message} name={"sex"} label="Sex">
<Select {...register("sex")}>
{Object.entries(Sex).map(([key, value]) => (
<option value={value}>{key}</option>
))}
</Select>
</FormField>
<Wrap spacing={2} padding="10px">
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
<Button value="Back" btnColorType="danger" href="/account" />
</Wrap>
</VStack>
</Container>
</form>
</Panel>
);
}
Expand Down
119 changes: 53 additions & 66 deletions src/pages/account/deletecharacter.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,49 @@
import React, { useEffect, useState, useCallback } from "react";
import Panel from "../../components/Panel";
import { withSessionSsr } from "../../lib/session";
import { fetchApi } from "../../lib/request";
import FormWrapper, { FormButton } from "../../components/FormWrapper";
import { deleteCharacterSchema } from "../../schemas/DeleteCharacter";
import { Select, Text } from "@chakra-ui/react";
import React from "react";
import Panel from "@component/Panel";
import { User, withSessionSsr } from "@lib/session";
import { Select, Text, Container, VStack, Wrap } from "@chakra-ui/react";
import { trpc } from "@util/trpc";
import { useFormFeedback } from "@hook/useFormFeedback";
import { SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import TextInput from "@component/TextInput";
import Button from "@component/Button";
import { FormField } from "@component/FormField";

const buttons: FormButton[] = [
{ type: "submit", btnColorType: "primary", value: "Submit" },
{ href: "/account", value: "Back" },
];
const schema = z.object({
name: z.string(),
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
});

interface DeleteCharacterProps {
user: { id: number };
export interface DeleteCharacterProps {
user: User;
}

export default function DeleteCharacter({ user }: DeleteCharacterProps) {
const [response, setResponse] = useState<any>(null);
const [data, setData] = useState<any>(null);

const fetchCharacters = useCallback(async () => {
const response = await fetchApi<any>("GET", `/api/account/${user.id}`);
if (response.success) {
setData({
fields: [
{
as: Select,
name: "name",
label: { text: "Name", size: 3 },
size: 9,
options: response.account.players.map((char: any) => ({
value: char.name,
text: char.name,
})),
},
{
type: "password",
name: "password",
label: { text: "Password", size: 3 },
size: 9,
},
],
initialValues: {
name: response.account.players[0]?.name,
password: "",
},
});
}
}, [user]);
const {
register,
handleSubmit,
reset,
formState: { errors, isValid, isSubmitting },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const account = trpc.account.singleById.useQuery({ id: user.id });
const deleteCharacter = trpc.account.deleteCharacter.useMutation();
const { showResponse, handleResponse } = useFormFeedback();

useEffect(() => {
fetchCharacters();
}, [fetchCharacters]);

if (!data) {
if (account.isLoading) {
return <Panel isLoading={true} />;
}

const onSubmit = async (values: any, { resetForm }: any) => {
const response = await fetchApi("POST", "/api/account/deletecharacter", {
data: {
name: values.name,
password: values.password,
},
const onSubmit: SubmitHandler<z.infer<typeof schema>> = async ({ name, password }) => {
handleResponse(async () => {
await deleteCharacter.mutateAsync({ name, password });
showResponse("Character deleted.", "success");
});

setResponse(response);
resetForm();
reset();
};

return (
Expand All @@ -75,14 +52,24 @@ export default function DeleteCharacter({ user }: DeleteCharacterProps) {
To delete a character choose the character and enter your password.
</Text>

<FormWrapper
validationSchema={deleteCharacterSchema}
onSubmit={onSubmit}
fields={data.fields}
buttons={buttons}
response={response}
initialValues={data.initialValues}
/>
<form onSubmit={handleSubmit(onSubmit)}>
<Container alignContent={"center"} padding={2}>
<VStack spacing={5}>
<FormField key={"name"} error={errors.name?.message} name={"name"} label="Character Name">
<Select {...register("name")}>
{account.data?.players.map((character) => <option value={character.name}>{character.name}</option>)}
</Select>
</FormField>
<FormField key={"password"} error={errors.password?.message} name={"password"} label={"Current Password"}>
<TextInput type="password" {...register("password")} />
</FormField>
<Wrap spacing={2} padding="10px">
<Button isLoading={isSubmitting} isActive={!isValid} loadingText="Submitting" type="submit" value="Submit" btnColorType="primary" />
<Button value="Back" btnColorType="danger" href="/account" />
</Wrap>
</VStack>
</Container>
</form>
</Panel>
);
}
Expand Down
6 changes: 5 additions & 1 deletion src/pages/account/register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ const fields = [
const schema = z
.object({
name: z.string().min(5, { message: "Account name must be at least 5 characters long" }),
password: z.string().min(6, { message: "Password must be at least 6 characters long" }),
password: z
.string()
.min(6, { message: "Password must be at least 6 characters long" })
.max(20, { message: "Password must be at most 20 characters long" })
.regex(/^[aA-zZ0-9]+$/, "Invalid letters, words or format. Use a-Z and spaces."),
repeatPassword: z.string(),
email: z.string().email({ message: "Invalid email address" }),
})
Expand Down
46 changes: 0 additions & 46 deletions src/pages/api/account/createcharacter.ts

This file was deleted.

Loading

0 comments on commit a500b63

Please sign in to comment.