Add ProfilePage, update RegisterForm and ToastBox components

Added new ProfilePage component with necessary UI layout and buttons. Updated RegisterForm component with form validation using Zod schema and improved error handling. Updated icons in ToastBox component for proper display. Added necessary dependencies for form handling and validation.
This commit is contained in:
Mathis H (Avnyr) 2024-05-21 12:28:03 +02:00
parent db6d838796
commit f6959724b0
Signed by: Mathis
GPG Key ID: DD9E0666A747D126
6 changed files with 156 additions and 86 deletions

29
.idea/workspace.xml generated
View File

@ -5,16 +5,12 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="42d2b9e8-8d19-46be-8636-69ddba4519e4" name="Changes" comment=""> <list default="true" id="42d2b9e8-8d19-46be-8636-69ddba4519e4" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/src/app/threads/page.tsx" afterDir="false" /> <change afterPath="$PROJECT_DIR$/src/app/profile/page.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/components/legals.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pnpm-lock.yaml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/pnpm-lock.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/app/layout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/app/layout.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/login-form.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/login-form.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/register-form.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/register-form.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/components/register-form.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/register-form.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ui/toast.tsx" beforeDir="false" /> <change beforePath="$PROJECT_DIR$/src/components/ui/toast-box.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ui/toast-box.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ui/toaster.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ui/use-toast.ts" beforeDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -24,13 +20,14 @@
<component name="FileTemplateManagerImpl"> <component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES"> <option name="RECENT_TEMPLATES">
<list> <list>
<option value="TypeScript JSX File" />
<option value="TypeScript File" /> <option value="TypeScript File" />
<option value="TypeScript JSX File" />
</list> </list>
</option> </option>
</component> </component>
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="UPDATE_TYPE" value="REBASE" />
</component> </component>
<component name="PackageJsonUpdateNotifier"> <component name="PackageJsonUpdateNotifier">
<dismissed value="$PROJECT_DIR$/package.json" /> <dismissed value="$PROJECT_DIR$/package.json" />
@ -48,7 +45,7 @@
"ASKED_ADD_EXTERNAL_FILES": "true", "ASKED_ADD_EXTERNAL_FILES": "true",
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"last_opened_file_path": "/home/avnyr/workspace/Simplon/brief-06-front", "last_opened_file_path": "/home/avnyr/workspace/Simplon/brief-06-back",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",
@ -91,10 +88,24 @@
<workItem from="1715776267209" duration="3260000" /> <workItem from="1715776267209" duration="3260000" />
<workItem from="1715781823049" duration="3317000" /> <workItem from="1715781823049" duration="3317000" />
<workItem from="1715794057604" duration="8757000" /> <workItem from="1715794057604" duration="8757000" />
<workItem from="1716275453494" duration="11394000" />
</task> </task>
<task id="LOCAL-00001" summary="Remove toast components and added register form&#10;&#10;The toast related components (toast.tsx, toaster.tsx, use-toast.ts) were removed from the code base. On the other hand, the register-form.tsx file was updated with a form for users to create an account. A new file toast-box.tsx was also added to display a custom message box.">
<option name="closed" value="true" />
<created>1715869879024</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1715869879024</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Remove toast components and added register form&#10;&#10;The toast related components (toast.tsx, toaster.tsx, use-toast.ts) were removed from the code base. On the other hand, the register-form.tsx file was updated with a form for users to create an account. A new file toast-box.tsx was also added to display a custom message box." />
<option name="LAST_COMMIT_MESSAGE" value="Remove toast components and added register form&#10;&#10;The toast related components (toast.tsx, toaster.tsx, use-toast.ts) were removed from the code base. On the other hand, the register-form.tsx file was updated with a form for users to create an account. A new file toast-box.tsx was also added to display a custom message box." />
</component>
</project> </project>

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@fontsource-variable/kode-mono": "^5.0.3", "@fontsource-variable/kode-mono": "^5.0.3",
"@fontsource/ubuntu": "^5.0.13", "@fontsource/ubuntu": "^5.0.13",
"@hookform/resolvers": "^3.4.2",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
@ -29,7 +30,8 @@
"react-hook-form": "^7.51.4", "react-hook-form": "^7.51.4",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

18
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ dependencies:
'@fontsource/ubuntu': '@fontsource/ubuntu':
specifier: ^5.0.13 specifier: ^5.0.13
version: 5.0.13 version: 5.0.13
'@hookform/resolvers':
specifier: ^3.4.2
version: 3.4.2(react-hook-form@7.51.4)
'@radix-ui/react-dropdown-menu': '@radix-ui/react-dropdown-menu':
specifier: ^2.0.6 specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1) version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.2)(react-dom@18.3.1)(react@18.3.1)
@ -68,6 +71,9 @@ dependencies:
tailwindcss-animate: tailwindcss-animate:
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.3) version: 1.0.7(tailwindcss@3.4.3)
zod:
specifier: ^3.23.8
version: 3.23.8
devDependencies: devDependencies:
'@types/node': '@types/node':
@ -138,6 +144,14 @@ packages:
resolution: {integrity: sha512-uCnfknLk6ERUUgGPHqvtLGkHkW/0oyxEJy6CZLaHbh6d1om1jvqcnc5nSPel1UCX5frLxRSTkRKU/vAbL9QtJw==} resolution: {integrity: sha512-uCnfknLk6ERUUgGPHqvtLGkHkW/0oyxEJy6CZLaHbh6d1om1jvqcnc5nSPel1UCX5frLxRSTkRKU/vAbL9QtJw==}
dev: false dev: false
/@hookform/resolvers@3.4.2(react-hook-form@7.51.4):
resolution: {integrity: sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==}
peerDependencies:
react-hook-form: ^7.0.0
dependencies:
react-hook-form: 7.51.4(react@18.3.1)
dev: false
/@isaacs/cliui@8.0.2: /@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1796,3 +1810,7 @@ packages:
resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==} resolution: {integrity: sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
hasBin: true hasBin: true
/zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
dev: false

19
src/app/profile/page.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from "react";
import {Button} from "@/components/ui/button";
import {KeyRound, Rss, UserRoundPlus} from "lucide-react";
export default function ProfilePage() {
return (
<main className="flex flex-col justify-evenly items-center antialiased h-full w-full">
<section className={"flex flex-col justify-evenly items-center md:h-1/3 mt-4 p-2 w-full h-full gap-1"}>
<h1 className={"text-4xl md:text-6xl font-bold"}>Only<em className={"text-primary opacity-65"}>Devs</em></h1>
<h2 className={"text-xl md:text-3xl text-center mt-1"}>A social network for you the <em className={"text-primary opacity-65"}>nerdy</em> developer !</h2>
<div className={"flex flex-col md:flex-row justify-evenly max-w-64 w-full md:max-w-full mt-2 gap-1"}>
<Button className={"mt-3 md:scale-125 gap-2 bg-purple-600"}><KeyRound />Login</Button>
<Button className={"mt-3 md:scale-125 gap-2 bg-purple-800"}><UserRoundPlus />Register</Button>
<Button className={"mt-3 md:scale-125 gap-2"}><Rss />Go see the posts</Button>
</div>
</section>
</main>
);
}

View File

@ -3,83 +3,97 @@ import React from "react";
import {Label} from "@/components/ui/label"; import {Label} from "@/components/ui/label";
import {Input} from "@/components/ui/input"; import {Input} from "@/components/ui/input";
import {cn} from "@/lib/utils"; import {cn} from "@/lib/utils";
import {OctagonX, User} from "lucide-react"; import {BadgeAlert, OctagonX, User} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import {Button} from "@/components/ui/button"; import {Button} from "@/components/ui/button";
import {Resolver, useForm} from "react-hook-form"; import {SubmitHandler, useForm} from "react-hook-form";
import toast, {ToastBar} from "react-hot-toast"; import toast, {ToastBar} from "react-hot-toast";
import {ToastBox, toastType} from "@/components/ui/toast-box"; import {ToastBox, toastType} from "@/components/ui/toast-box";
import {zodResolver} from "@hookform/resolvers/zod"
import {z} from 'zod'
interface UserInsert { const Schema = z.object({
username: string, username:
displayName?: string, z.string()
email: string, .min(3, {
password: string, message: 'The username should be at least 3 character(s).'
rePassword: string })
error?: Record<string, { type: string; message: string }> .max(14, {
} message: 'The username should be at most 14 character(s).'
}),
displayName:
z.string()
.min(3, {
message: 'The display name should be at least 3 character(s).'
})
.max(20, {
message: 'The display name should be at most 20 character(s).'
})
.optional(),
email:
z.string()
.email({
message: 'Please enter a valid email address.'
})
.max(32, {
message: 'The email should be less than 32 character(s).'
}),
password:
z.string()
.min(8, {
message: 'The password should be at least 8 character(s).'
})
.max(32, {
message: 'The password should be less than 32 character(s).'
}),
rePassword:
z.string()
}).superRefine(({ rePassword, password }, ctx) => {
if (rePassword !== password) {
ctx.addIssue({
code: "custom",
message: "The passwords did not match",
path: ['rePassword']
});
}});
export const resolver: Resolver<UserInsert> = async (values) => { type UserInsert = z.infer<typeof Schema>
const errors: Record<string, { type: string; message: string }> = {}
if (!values.username) { function ErrorLabel({message}: {message: string}) {
errors.name = { return (
type: 'required', <div className={'flex flex-row items-center gap-1 text-sm'}>
message: 'The username is required.', <BadgeAlert className={'text-red-700'}/>
} <p className={'text-red-700'}>{message}</p>
} </div>
)
if (!values.displayName) {
errors.description = {
type: 'required',
message: 'The displayname is required.',
}
}
if (!values.email) {
errors.start = {
type: 'required',
message: 'The email is required.',
}
}
if (!values.password) {
errors.promoId = {
type: 'required',
message: 'The password is required.',
}
}
if (!values.rePassword) {
errors.promoId = {
type: 'required',
message: 'You should retype the password.',
}
}
return {
values: Object.keys(errors).length === 0 ? values : {},
errors,
}
} }
export function RegisterForm() { export function RegisterForm() {
const handleSubbmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("Form submitted");
};
const { register, handleSubmit, getValues } = useForm(); const {
register,
handleSubmit,
getValues,
setError,
formState: {errors,
isSubmitting} } = useForm<UserInsert>({
resolver: zodResolver(Schema)
});
const onSubmit = (data: UserInsert) => { const onSubmit: SubmitHandler<UserInsert> = async (data) => {
console.log(data); try {
if (!data.error) { await new Promise((resolve) => setTimeout(resolve, 1_000))
toast.custom(<ToastBox toast.custom(<ToastBox
message={'Hello world !'} title={`Verify your email`}
message={`Please verify you mail inbox to activate your account.`}
type={toastType.success} type={toastType.success}
/>) />)
console.log(data);
} catch (err) {
setError('root', {message: 'An error occurred.'})
} }
}; }
return ( return (
<div className="max-w-md w-full mx-auto rounded-none md:rounded-2xl p-4 md:p-8 shadow-input md:scale-125"> <div className="max-w-md w-full mx-auto rounded-none md:rounded-2xl p-4 md:p-8 shadow-input md:scale-125">
<h2 className="font-bold text-xl text-neutral-800 dark:text-neutral-200"> <h2 className="font-bold text-xl text-neutral-800 dark:text-neutral-200">
@ -90,14 +104,14 @@ export function RegisterForm() {
</p> </p>
<form className="my-8" onSubmit={handleSubmit(onSubmit)}> <form className="my-8" onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4"> <div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-1">
<LabelInputContainer className="mb-4"> <LabelInputContainer className="mb-4">
<Label htmlFor="username">Username<em className={"opacity-50"}>*</em></Label> <Label htmlFor="username">Username<em className={"opacity-50"}>*</em></Label>
<Input <Input
id="username" id="username"
placeholder="john.doe68" placeholder="john.doe68"
type="text" type="text"
{...register("username", { required: true })} {...register("username")}
/> />
</LabelInputContainer> </LabelInputContainer>
<LabelInputContainer className="mb-4"> <LabelInputContainer className="mb-4">
@ -106,10 +120,11 @@ export function RegisterForm() {
id="displayName" id="displayName"
placeholder="Johny" placeholder="Johny"
type="text" type="text"
{...register("displayName")} {...register("displayName", {required: false})}
/> />
</LabelInputContainer> </LabelInputContainer>
</div> </div>
{errors.username && <ErrorLabel message={errors.username?.message || ''} />}
<div <div
className="bg-gradient-to-r from-transparent via-neutral-300 dark:via-neutral-700 to-transparent my-4 h-[1px] w-full"/> className="bg-gradient-to-r from-transparent via-neutral-300 dark:via-neutral-700 to-transparent my-4 h-[1px] w-full"/>
<LabelInputContainer className="mb-4"> <LabelInputContainer className="mb-4">
@ -118,8 +133,9 @@ export function RegisterForm() {
id="email" id="email"
placeholder="john.doe@foo.bar" placeholder="john.doe@foo.bar"
type="email" type="email"
{...register("email", { required: true })} {...register("email")}
/> />
{errors.email && <ErrorLabel message={errors.email?.message || ''} />}
</LabelInputContainer> </LabelInputContainer>
<LabelInputContainer className="mb-4"> <LabelInputContainer className="mb-4">
<Label htmlFor="password">Password<em className={"opacity-50"}>*</em></Label> <Label htmlFor="password">Password<em className={"opacity-50"}>*</em></Label>
@ -127,24 +143,28 @@ export function RegisterForm() {
id="password" id="password"
placeholder="••••••••" placeholder="••••••••"
type="password" type="password"
{...register("password", { required: true })} {...register("password")}
/> />
{errors.password && <ErrorLabel message={errors.password?.message || ''} />}
</LabelInputContainer> </LabelInputContainer>
<LabelInputContainer className="mb-8"> <LabelInputContainer className="mb-8">
<Label htmlFor="re-password">Re-type password<em className={"opacity-50"}>*</em></Label> <Label htmlFor="re-password">Re-type password<em className={"opacity-50"}>*</em></Label>
<Input <Input
id="re-password" id="rePassword"
placeholder="••••••••" placeholder="••••••••"
type="password" type="password"
{...register("re-password", { validate: (value) => value === getValues('password') || "The passwords do not match" })} {...register("rePassword", {required: 'You need fill again our password here.', validate: (value) => value === getValues('password') || "The passwords do not match." })}
/> />
{errors.rePassword && <ErrorLabel message={errors.rePassword?.message || ''} />}
</LabelInputContainer> </LabelInputContainer>
<div className={'flex flex-row justify-center items-center'}>
{errors.root && <ErrorLabel message={errors.root?.message || ''} />}
</div>
<button <button
className="bg-gradient-to-br relative group/btn from-black dark:from-zinc-900 dark:to-zinc-900 to-neutral-600 block dark:bg-zinc-800 w-full text-white rounded-md h-10 font-medium shadow-[0px_1px_0px_0px_#ffffff40_inset,0px_-1px_0px_0px_#ffffff40_inset] dark:shadow-[0px_1px_0px_0px_var(--zinc-800)_inset,0px_-1px_0px_0px_var(--zinc-800)_inset]" className="bg-gradient-to-br relative group/btn from-black dark:from-zinc-900 dark:to-zinc-900 to-neutral-600 block dark:bg-zinc-800 w-full text-white rounded-md h-10 font-medium shadow-[0px_1px_0px_0px_#ffffff40_inset,0px_-1px_0px_0px_#ffffff40_inset] dark:shadow-[0px_1px_0px_0px_var(--zinc-800)_inset,0px_-1px_0px_0px_var(--zinc-800)_inset]"
type="submit" type="submit"
> >
Register &rarr; {!isSubmitting ? 'Register' : 'Loading...'}
<BottomGradient/> <BottomGradient/>
</button> </button>
<div <div

View File

@ -1,4 +1,4 @@
import {CircleCheckBig, MessageSquareText, OctagonX} from "lucide-react"; import {Bug, CircleAlert, CircleCheckBig, CircleHelp, MessageSquareText, Minus, OctagonX, Plus} from "lucide-react";
import React from "react"; import React from "react";
@ -22,23 +22,23 @@ export function ToastBox({title, message, type}: {title?: string, message: strin
bgColor = 'bg-accent' bgColor = 'bg-accent'
break break
case toastType.add: case toastType.add:
icon = '<Plus />' icon = <Plus />
bgColor = 'bg-accent' bgColor = 'bg-accent'
break break
case toastType.del: case toastType.del:
icon = '<Minus />' icon = <Minus />
bgColor = 'bg-accent' bgColor = 'bg-accent'
break break
case toastType.info: case toastType.info:
icon = '<CircleHelp />' icon = <CircleHelp />
bgColor = 'bg-accent' bgColor = 'bg-accent'
break break
case toastType.warn: case toastType.warn:
icon = '<CircleAlert />' icon = <CircleAlert />
bgColor = 'bg-orange-500' bgColor = 'bg-orange-500'
break break
case toastType.error: case toastType.error:
icon = '<Bug />' icon = <Bug />
bgColor = 'bg-red-500' bgColor = 'bg-red-500'
break break
case toastType.success: case toastType.success:
@ -46,7 +46,7 @@ export function ToastBox({title, message, type}: {title?: string, message: strin
bgColor = 'bg-green-500' bgColor = 'bg-green-500'
break break
case toastType.refused: case toastType.refused:
icon = '<OctagonX />' icon = <OctagonX />
bgColor = 'bg-red-700' bgColor = 'bg-red-700'
break break
} }