mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Update: update confirm user, user account page
This commit is contained in:
@@ -47,116 +47,123 @@ const AccountInfo: FC = () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, handleSubmit, formState, reset } = useForm<
|
const [editToggled, setEditToggled] = useState(false);
|
||||||
z.infer<typeof EditUserSchema>
|
|
||||||
>({
|
|
||||||
resolver: zodResolver(EditUserSchema),
|
|
||||||
defaultValues: {
|
|
||||||
username: user!.username,
|
|
||||||
email: user!.email,
|
|
||||||
firstName: user!.firstName,
|
|
||||||
lastName: user!.lastName,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [inEditMode, setInEditMode] = useState(false);
|
|
||||||
|
|
||||||
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
|
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
|
||||||
const loadingToast = toast.loading('Submitting edits...');
|
const loadingToast = toast.loading('Submitting edits...');
|
||||||
try {
|
try {
|
||||||
await sendEditUserRequest({ user: user!, data });
|
await sendEditUserRequest({ user: user!, data });
|
||||||
await mutate!();
|
|
||||||
setInEditMode(false);
|
|
||||||
toast.remove(loadingToast);
|
toast.remove(loadingToast);
|
||||||
toast.success('Edits submitted successfully.');
|
toast.success('Edits submitted successfully.');
|
||||||
|
setEditToggled(false);
|
||||||
|
await mutate!();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setInEditMode(false);
|
setEditToggled(false);
|
||||||
toast.remove(loadingToast);
|
toast.remove(loadingToast);
|
||||||
createErrorToast(error);
|
createErrorToast(error);
|
||||||
await mutate!();
|
await mutate!();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const { register, handleSubmit, formState, reset } = useForm<
|
||||||
|
z.infer<typeof EditUserSchema>
|
||||||
|
>({
|
||||||
|
resolver: zodResolver(EditUserSchema),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8">
|
<div className="card mt-8">
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="card-body flex flex-col space-y-3">
|
||||||
<form
|
<div className="flex w-full items-center justify-between space-x-5">
|
||||||
className="form-control space-y-5"
|
<div className="">
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
<h1 className="text-lg font-bold">Edit Your Account Info</h1>
|
||||||
noValidate
|
<p>Update your personal account information.</p>
|
||||||
>
|
|
||||||
<label className="label w-36 cursor-pointer p-0">
|
|
||||||
<span className="label-text font-bold uppercase">Enable Edit</span>
|
|
||||||
<Switch
|
|
||||||
checked={inEditMode}
|
|
||||||
className="toggle"
|
|
||||||
onClick={() => {
|
|
||||||
setInEditMode((editMode) => !editMode);
|
|
||||||
reset();
|
|
||||||
}}
|
|
||||||
id="edit-toggle"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<FormInfo>
|
|
||||||
<FormLabel htmlFor="username">Username</FormLabel>
|
|
||||||
<FormError>{formState.errors.username?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<FormTextInput
|
|
||||||
type="text"
|
|
||||||
disabled={!inEditMode || formState.isSubmitting}
|
|
||||||
error={!!formState.errors.username}
|
|
||||||
id="username"
|
|
||||||
formValidationSchema={register('username')}
|
|
||||||
/>
|
|
||||||
<FormInfo>
|
|
||||||
<FormLabel htmlFor="email">Email</FormLabel>
|
|
||||||
<FormError>{formState.errors.email?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<FormTextInput
|
|
||||||
type="email"
|
|
||||||
disabled={!inEditMode || formState.isSubmitting}
|
|
||||||
error={!!formState.errors.email}
|
|
||||||
id="email"
|
|
||||||
formValidationSchema={register('email')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
|
||||||
<div className="w-1/2">
|
|
||||||
<FormInfo>
|
|
||||||
<FormLabel htmlFor="firstName">First Name</FormLabel>
|
|
||||||
<FormError>{formState.errors.firstName?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<FormTextInput
|
|
||||||
type="text"
|
|
||||||
disabled={!inEditMode || formState.isSubmitting}
|
|
||||||
error={!!formState.errors.firstName}
|
|
||||||
id="firstName"
|
|
||||||
formValidationSchema={register('firstName')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/2">
|
|
||||||
<FormInfo>
|
|
||||||
<FormLabel htmlFor="lastName">Last Name</FormLabel>
|
|
||||||
<FormError>{formState.errors.lastName?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<FormTextInput
|
|
||||||
type="text"
|
|
||||||
disabled={!inEditMode || formState.isSubmitting}
|
|
||||||
error={!!formState.errors.lastName}
|
|
||||||
id="lastName"
|
|
||||||
formValidationSchema={register('lastName')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{inEditMode && (
|
<div>
|
||||||
<button className="btn-primary btn w-full" type="submit">
|
<Switch
|
||||||
Save Changes
|
className="toggle"
|
||||||
</button>
|
id="edit-toggle"
|
||||||
)}
|
checked={editToggled}
|
||||||
</form>
|
onClick={async () => {
|
||||||
|
setEditToggled((val) => !val);
|
||||||
|
await mutate!();
|
||||||
|
reset({
|
||||||
|
username: user!.username,
|
||||||
|
email: user!.email,
|
||||||
|
firstName: user!.firstName,
|
||||||
|
lastName: user!.lastName,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editToggled && (
|
||||||
|
<form
|
||||||
|
className="form-control space-y-5"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="username">Username</FormLabel>
|
||||||
|
<FormError>{formState.errors.username?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormTextInput
|
||||||
|
type="text"
|
||||||
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
|
error={!!formState.errors.username}
|
||||||
|
id="username"
|
||||||
|
formValidationSchema={register('username')}
|
||||||
|
/>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="email">Email</FormLabel>
|
||||||
|
<FormError>{formState.errors.email?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormTextInput
|
||||||
|
type="email"
|
||||||
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
|
error={!!formState.errors.email}
|
||||||
|
id="email"
|
||||||
|
formValidationSchema={register('email')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="firstName">First Name</FormLabel>
|
||||||
|
<FormError>{formState.errors.firstName?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormTextInput
|
||||||
|
type="text"
|
||||||
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
|
error={!!formState.errors.firstName}
|
||||||
|
id="firstName"
|
||||||
|
formValidationSchema={register('firstName')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="lastName">Last Name</FormLabel>
|
||||||
|
<FormError>{formState.errors.lastName?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormTextInput
|
||||||
|
type="text"
|
||||||
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
|
error={!!formState.errors.lastName}
|
||||||
|
id="lastName"
|
||||||
|
formValidationSchema={register('lastName')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn-primary btn my-5 w-full"
|
||||||
|
type="submit"
|
||||||
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,56 +20,65 @@ const Security: FunctionComponent = () => {
|
|||||||
|
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
|
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
|
||||||
await sendUpdatePasswordRequest(data);
|
await sendUpdatePasswordRequest(data);
|
||||||
|
setEditToggled(value => !value)
|
||||||
reset();
|
reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 w-full space-y-4">
|
<div className="card w-full space-y-4">
|
||||||
<div className="flex w-full items-center justify-between space-x-5">
|
<div className="card-body">
|
||||||
<div className="">
|
<div className="flex w-full items-center justify-between space-x-5">
|
||||||
<h1 className="text-lg font-bold">Change Your Password</h1>
|
<div className="">
|
||||||
<p>Update your password to maintain the safety of your account.</p>
|
<h1 className="text-lg font-bold">Change Your Password</h1>
|
||||||
|
<p>Update your password to maintain the safety of your account.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
className="toggle"
|
||||||
|
id="edit-toggle"
|
||||||
|
checked={editToggled}
|
||||||
|
onClick={() => {
|
||||||
|
setEditToggled((val) => !val);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{editToggled && (
|
||||||
<Switch
|
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||||
className="toggle"
|
<FormInfo>
|
||||||
id="edit-toggle"
|
<FormLabel htmlFor="password">New Password</FormLabel>
|
||||||
checked={editToggled}
|
<FormError>{formState.errors.password?.message}</FormError>
|
||||||
onClick={() => setEditToggled((val) => !val)}
|
</FormInfo>
|
||||||
/>
|
<FormTextInput
|
||||||
</div>
|
type="password"
|
||||||
</div>
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
{editToggled && (
|
error={!!formState.errors.password}
|
||||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
id="password"
|
||||||
<FormInfo>
|
formValidationSchema={register('password')}
|
||||||
<FormLabel htmlFor="password">New Password</FormLabel>
|
/>
|
||||||
<FormError>{formState.errors.password?.message}</FormError>
|
<FormInfo>
|
||||||
</FormInfo>
|
<FormLabel htmlFor="password">Confirm Password</FormLabel>
|
||||||
<FormTextInput
|
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
||||||
type="password"
|
</FormInfo>
|
||||||
disabled={!editToggled || formState.isSubmitting}
|
<FormTextInput
|
||||||
error={!!formState.errors.password}
|
type="password"
|
||||||
id="password"
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
formValidationSchema={register('password')}
|
error={!!formState.errors.confirmPassword}
|
||||||
/>
|
id="password"
|
||||||
<FormInfo>
|
formValidationSchema={register('confirmPassword')}
|
||||||
<FormLabel htmlFor="password">Confirm Password</FormLabel>
|
/>
|
||||||
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
|
||||||
</FormInfo>
|
|
||||||
<FormTextInput
|
|
||||||
type="password"
|
|
||||||
disabled={!editToggled || formState.isSubmitting}
|
|
||||||
error={!!formState.errors.confirmPassword}
|
|
||||||
id="password"
|
|
||||||
formValidationSchema={register('confirmPassword')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button className="btn-primary btn mt-5" disabled={!editToggled} type="submit">
|
<button
|
||||||
Update
|
className="btn-primary btn mt-5"
|
||||||
</button>
|
disabled={!editToggled || formState.isSubmitting}
|
||||||
</form>
|
type="submit"
|
||||||
)}
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ const toastToClassName = (toastType: Toast['type']) => {
|
|||||||
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toaster position="bottom-center">
|
<Toaster position="bottom-right">
|
||||||
{(t) => {
|
{(t) => {
|
||||||
const alertType = toastToClassName(t.type);
|
const alertType = toastToClassName(t.type);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`alert ${alertType} w-11/12 flex-row items-center shadow-lg animate-in fade-in duration-200 lg:w-6/12`}
|
className={`alert ${alertType} w-11/12 flex-row items-center shadow-lg animate-in fade-in duration-200 lg:w-2/12`}
|
||||||
>
|
>
|
||||||
<p>{resolveValue(t.message, t)}</p>
|
<p className='text-sm'>{resolveValue(t.message, t)}</p>
|
||||||
{t.type !== 'loading' && (
|
{t.type !== 'loading' && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
69
src/hooks/auth/useConfirmUser.ts
Normal file
69
src/hooks/auth/useConfirmUser.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState, useContext, useEffect } from 'react';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
const useConfirmUser = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { user, mutate } = useContext(UserContext);
|
||||||
|
const token = router.query.token as string | undefined;
|
||||||
|
const [needsToLogin, setNeedsToLogin] = useState(false);
|
||||||
|
const [tokenInvalid, setTokenInvalid] = useState(false);
|
||||||
|
|
||||||
|
const fetcher = async <T extends string>(url: T) => {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Token must be provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('API response validation failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
mutate!();
|
||||||
|
return parsed.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = useSWR(`/api/users/confirm?token=${token}`, fetcher);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadingToast = toast.loading('Attempting to confirm your account.');
|
||||||
|
if (user && user.accountIsVerified) {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
router.replace('/users/current');
|
||||||
|
toast('Your account is already verified.');
|
||||||
|
}
|
||||||
|
if (!token) {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
setTokenInvalid(true);
|
||||||
|
setNeedsToLogin(false);
|
||||||
|
}
|
||||||
|
if (user && !user.accountIsVerified && !data) {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
setTokenInvalid(true);
|
||||||
|
setNeedsToLogin(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message === 'Unauthorized') {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
setTokenInvalid(false);
|
||||||
|
setNeedsToLogin(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
};
|
||||||
|
}, [error, data, router, user, token]);
|
||||||
|
|
||||||
|
return { needsToLogin, tokenInvalid };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useConfirmUser;
|
||||||
@@ -21,7 +21,7 @@ const AccountPage: NextPage = () => {
|
|||||||
content="Your account page. Here you can view your account information, change your settings, and view your posts."
|
content="Your account page. Here you can view your account information, change your settings, and view your posts."
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex h-full flex-col items-center bg-base-300">
|
<div className="flex flex-col items-center">
|
||||||
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-7/12">
|
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-7/12">
|
||||||
<div className="flex flex-col items-center space-y-3">
|
<div className="flex flex-col items-center space-y-3">
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
@@ -34,24 +34,19 @@ const AccountPage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="h-full w-full">
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
||||||
<Tab className="tab tab-md w-1/3 uppercase ui-selected:tab-active">
|
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||||
Account Info
|
Account Info and Security
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab className="tab tab-md w-1/3 uppercase ui-selected:tab-active">
|
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||||
Security
|
|
||||||
</Tab>
|
|
||||||
<Tab className="tab tab-md w-1/3 uppercase ui-selected:tab-active">
|
|
||||||
Your Posts
|
Your Posts
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels>
|
<Tab.Panels>
|
||||||
<Tab.Panel>
|
<Tab.Panel className="h-full space-y-5">
|
||||||
<AccountInfo />
|
<AccountInfo />
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
|
||||||
<Security />
|
<Security />
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel>Your posts!</Tab.Panel>
|
<Tab.Panel>Your posts!</Tab.Panel>
|
||||||
|
|||||||
@@ -1,125 +1,73 @@
|
|||||||
import UserContext from '@/contexts/UserContext';
|
import useConfirmUser from '@/hooks/auth/useConfirmUser';
|
||||||
import createErrorToast from '@/util/createErrorToast';
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { FC, useContext, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
const useSendConfirmUserRequest = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const token = router.query.token as string | undefined;
|
|
||||||
|
|
||||||
const { data, error } = useSWR(`/api/users/confirm?token=${token}`, async (url) => {
|
|
||||||
if (!token) {
|
|
||||||
throw new Error('Token must be provided.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
||||||
const json = await response.json();
|
|
||||||
|
|
||||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
throw new Error('API response validation failed.');
|
|
||||||
}
|
|
||||||
return parsed.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data, error: error as unknown };
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConfirmUserPage: FC = () => {
|
const ConfirmUserPage: FC = () => {
|
||||||
const router = useRouter();
|
const { needsToLogin, tokenInvalid } = useConfirmUser();
|
||||||
const { error, data } = useSendConfirmUserRequest();
|
|
||||||
const { user } = useContext(UserContext);
|
|
||||||
|
|
||||||
const needsToLogin =
|
|
||||||
error instanceof Error && error.message === 'Unauthorized' && !user;
|
|
||||||
const tokenExpired = error instanceof Error && error.message === 'Unauthorized' && user;
|
|
||||||
const [confirmationResent, setConfirmationResent] = useState(false);
|
const [confirmationResent, setConfirmationResent] = useState(false);
|
||||||
|
const onClick = async () => {
|
||||||
|
const resentConfirmationLoadingToast = toast.loading(
|
||||||
|
'Resending your confirmation email.',
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/users/resend-confirmation', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Something went wrong.');
|
||||||
|
}
|
||||||
|
|
||||||
if (user?.accountIsVerified) {
|
toast.remove(resentConfirmationLoadingToast);
|
||||||
router.push('/users/current');
|
toast.success('Sent a new confirmation email.');
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data) {
|
setConfirmationResent(true);
|
||||||
router.push('/users/current');
|
} catch (err) {
|
||||||
return null;
|
createErrorToast(err);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (needsToLogin) {
|
return (
|
||||||
return (
|
<>
|
||||||
<>
|
<Head>
|
||||||
<Head>
|
<title>Confirm User | The Biergarten App</title>
|
||||||
<title>Confirm User | The Biergarten App</title>
|
</Head>
|
||||||
</Head>
|
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
||||||
<div className="flex h-full flex-col items-center justify-center">
|
{needsToLogin && (
|
||||||
<p className="text-center text-xl font-bold">
|
<p className="text-center text-xl font-bold">
|
||||||
Please login to confirm your account.
|
Please login to confirm your account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (tokenExpired) {
|
|
||||||
const onClick = async () => {
|
|
||||||
const loadingToast = toast.loading('Resending your confirmation email.');
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/users/resend-confirmation', {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Something went wrong.');
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.remove(loadingToast);
|
{!needsToLogin && tokenInvalid && !confirmationResent && (
|
||||||
toast.success('Sent a new confirmation email.');
|
<>
|
||||||
|
<p className="text-center text-2xl font-bold">
|
||||||
|
Your confirmation token is invalid or is expired.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
className="btn-outline btn-sm btn normal-case"
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Click here to request a new token.
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
setConfirmationResent(true);
|
{!needsToLogin && tokenInvalid && confirmationResent && (
|
||||||
} catch (err) {
|
<>
|
||||||
createErrorToast(err);
|
<p className="text-center text-2xl font-bold">
|
||||||
}
|
Resent your confirmation link.
|
||||||
};
|
</p>
|
||||||
|
<p className="font-xl text-center">Please check your email.</p>
|
||||||
return (
|
</>
|
||||||
<>
|
)}
|
||||||
<Head>
|
</div>
|
||||||
<title>Confirm User | The Biergarten App</title>
|
</>
|
||||||
</Head>
|
);
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
|
||||||
{!confirmationResent ? (
|
|
||||||
<>
|
|
||||||
<p className="text-center text-2xl font-bold">
|
|
||||||
Your confirmation token is expired.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className="btn-outline btn-sm btn normal-case"
|
|
||||||
onClick={onClick}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Click here to request a new token.
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<p className="text-center text-2xl font-bold">
|
|
||||||
Resent your confirmation link.
|
|
||||||
</p>
|
|
||||||
<p className="font-xl text-center">Please check your email.</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfirmUserPage;
|
export default ConfirmUserPage;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const ProtectedPage: NextPage = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Hello, {user?.firstName}! | The Biergarten App</title>
|
<title>Hello! | The Biergarten App</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-3 bg-primary text-center">
|
<div className="flex h-full flex-col items-center justify-center space-y-3 bg-primary text-center">
|
||||||
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import sub from 'date-fns/sub';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const MINIMUM_DATE_OF_BIRTH = sub(new Date(), { years: 19 });
|
const MINIMUM_DATE_OF_BIRTH = sub(new Date(), { years: 19 });
|
||||||
|
const NAME_REGEX =
|
||||||
|
/^[a-zA-ZàáâäãåąčćęèéêëėįìíîïłńòóôöõøùúûüųūÿýżźçčšžÀÁÂÄÃÅĄĆČĖĘÈÉÊËÌÍÎÏĮŁŃÒÓÔÖÕØÙÚÛÜŲŪŸÝŻŹÑßÇŒÆČŠŽ∂ðæ ,.'-]+$/u;
|
||||||
|
|
||||||
export const BaseCreateUserSchema = z.object({
|
export const BaseCreateUserSchema = z.object({
|
||||||
password: z
|
password: z
|
||||||
@@ -23,14 +25,14 @@ export const BaseCreateUserSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'First name must not be empty.' })
|
.min(1, { message: 'First name must not be empty.' })
|
||||||
.max(20, { message: 'First name must be less than 20 characters.' })
|
.max(20, { message: 'First name must be less than 20 characters.' })
|
||||||
.refine((firstName) => /^[a-zA-Z]+$/.test(firstName), {
|
.refine((firstName) => NAME_REGEX.test(firstName), {
|
||||||
message: 'First name must only contain letters.',
|
message: 'First name must only contain letters or hyphens.',
|
||||||
}),
|
}),
|
||||||
lastName: z
|
lastName: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: 'Last name must not be empty.' })
|
.min(1, { message: 'Last name must not be empty.' })
|
||||||
.max(20, { message: 'Last name must be less than 20 characters.' })
|
.max(20, { message: 'Last name must be less than 20 characters.' })
|
||||||
.refine((lastName) => /^[a-zA-Z]+$/.test(lastName), {
|
.refine((lastName) => NAME_REGEX.test(lastName), {
|
||||||
message: 'Last name must only contain letters.',
|
message: 'Last name must only contain letters.',
|
||||||
}),
|
}),
|
||||||
dateOfBirth: z
|
dateOfBirth: z
|
||||||
|
|||||||
Reference in New Issue
Block a user