Update: add more toast notifications, update position

Also set Account page to use UserContext. Refactored api requests out of components.
This commit is contained in:
Aaron William Po
2023-05-22 22:41:37 -04:00
parent 27e72d3dcf
commit 4c30af27b6
16 changed files with 242 additions and 188 deletions

View File

@@ -1,24 +1,23 @@
import validateEmailRequest from '@/requests/User/validateEmailRequest'; import validateEmailRequest from '@/requests/User/validateEmailRequest';
import validateUsernameRequest from '@/requests/validateUsernameRequest'; import validateUsernameRequest from '@/requests/validateUsernameRequest';
import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas'; import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router'; import { FC, useContext, useState } from 'react';
import { FC, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import UserContext from '@/contexts/UserContext';
import sendEditUserRequest from '@/requests/User/sendEditUserRequest';
import createErrorToast from '@/util/createErrorToast';
import { toast } from 'react-hot-toast';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel'; import FormLabel from '../ui/forms/FormLabel';
import FormTextInput from '../ui/forms/FormTextInput'; import FormTextInput from '../ui/forms/FormTextInput';
interface AccountInfoProps { const AccountInfo: FC = () => {
user: z.infer<typeof GetUserSchema>; const { user, mutate } = useContext(UserContext);
}
const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
const router = useRouter();
const EditUserSchema = BaseCreateUserSchema.pick({ const EditUserSchema = BaseCreateUserSchema.pick({
username: true, username: true,
email: true, email: true,
@@ -30,7 +29,7 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
.email({ message: 'Email must be a valid email address.' }) .email({ message: 'Email must be a valid email address.' })
.refine( .refine(
async (email) => { async (email) => {
if (user.email === email) return true; if (user!.email === email) return true;
return validateEmailRequest(email); return validateEmailRequest(email);
}, },
{ message: 'Email is already taken.' }, { message: 'Email is already taken.' },
@@ -41,7 +40,7 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
.max(20, { message: 'Username must be less than 20 characters.' }) .max(20, { message: 'Username must be less than 20 characters.' })
.refine( .refine(
async (username) => { async (username) => {
if (user.username === username) return true; if (user!.username === username) return true;
return validateUsernameRequest(username); return validateUsernameRequest(username);
}, },
{ message: 'Username is already taken.' }, { message: 'Username is already taken.' },
@@ -53,29 +52,29 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
>({ >({
resolver: zodResolver(EditUserSchema), resolver: zodResolver(EditUserSchema),
defaultValues: { defaultValues: {
username: user.username, username: user!.username,
email: user.email, email: user!.email,
firstName: user.firstName, firstName: user!.firstName,
lastName: user.lastName, lastName: user!.lastName,
}, },
}); });
const [inEditMode, setInEditMode] = useState(false); const [inEditMode, setInEditMode] = useState(false);
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => { const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
const response = await fetch(`/api/users/${user.id}/edit`, { const loadingToast = toast.loading('Submitting edits...');
body: JSON.stringify(data), try {
method: 'PUT', await sendEditUserRequest({ user: user!, data });
headers: { 'Content-Type': 'application/json' }, await mutate!();
}); setInEditMode(false);
toast.remove(loadingToast);
if (!response.ok) { toast.success('Edits submitted successfully.');
throw new Error('Something went wrong.'); } catch (error) {
setInEditMode(false);
toast.remove(loadingToast);
createErrorToast(error);
await mutate!();
} }
await response.json();
router.reload();
}; };
return ( return (

View File

@@ -2,7 +2,8 @@ import UserContext from '@/contexts/UserContext';
import useTimeDistance from '@/hooks/utilities/useTimeDistance'; import useTimeDistance from '@/hooks/utilities/useTimeDistance';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Dispatch, FC, SetStateAction, useContext } from 'react'; import { Dispatch, FC, SetStateAction, useContext } from 'react';
import { Link, Rating } from 'react-daisyui'; import { Rating } from 'react-daisyui';
import Link from 'next/link';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import { z } from 'zod'; import { z } from 'zod';

View File

@@ -8,6 +8,7 @@ import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResul
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments'; import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel'; import FormLabel from '../ui/forms/FormLabel';
@@ -31,7 +32,6 @@ interface EditCommentBodyProps {
const EditCommentBody: FC<EditCommentBodyProps> = ({ const EditCommentBody: FC<EditCommentBodyProps> = ({
comment, comment,
setInEditMode, setInEditMode,
mutate, mutate,
handleDeleteRequest, handleDeleteRequest,
handleEditRequest, handleEditRequest,
@@ -43,24 +43,41 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
resolver: zodResolver(CreateCommentValidationSchema), resolver: zodResolver(CreateCommentValidationSchema),
}); });
const { errors } = formState; const { errors, isSubmitting } = formState;
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const onDelete = async () => { const onDelete = async () => {
const loadingToast = toast.loading('Deleting comment...');
setIsDeleting(true); setIsDeleting(true);
await handleDeleteRequest(comment.id); try {
await mutate(); await handleDeleteRequest(comment.id);
await mutate();
toast.remove(loadingToast);
toast.success('Deleted comment.');
} catch (error) {
toast.remove(loadingToast);
createErrorToast(error);
}
}; };
const onEdit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async ( const onEdit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
data, data,
) => { ) => {
setInEditMode(true); const loadingToast = toast.loading('Submitting comment edits...');
await handleEditRequest(comment.id, data);
await mutate(); try {
toast.success('Submitted edits'); setInEditMode(true);
setInEditMode(false); await handleEditRequest(comment.id, data);
await mutate();
toast.remove(loadingToast);
toast.success('Comment edits submitted successfully.');
setInEditMode(false);
} catch (error) {
toast.remove(loadingToast);
createErrorToast(error);
setInEditMode(false);
}
}; };
return ( return (
@@ -78,7 +95,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
placeholder="Comment" placeholder="Comment"
rows={2} rows={2}
error={!!errors.content?.message} error={!!errors.content?.message}
disabled={formState.isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
/> />
</FormSegment> </FormSegment>
<div className="flex flex-row items-center justify-between"> <div className="flex flex-row items-center justify-between">
@@ -97,8 +114,8 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
<Rating.Item <Rating.Item
name="rating-1" name="rating-1"
className="mask mask-star cursor-default" className="mask mask-star cursor-default"
disabled={formState.isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
aria-disabled={formState.isSubmitting || isDeleting} aria-disabled={isSubmitting || isDeleting}
key={index} key={index}
/> />
))} ))}
@@ -108,7 +125,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
<button <button
type="button" type="button"
className="btn-xs btn lg:btn-sm" className="btn-xs btn lg:btn-sm"
disabled={formState.isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
onClick={() => { onClick={() => {
setInEditMode(false); setInEditMode(false);
}} }}
@@ -117,7 +134,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
</button> </button>
<button <button
type="submit" type="submit"
disabled={formState.isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
className="btn-xs btn lg:btn-sm" className="btn-xs btn lg:btn-sm"
> >
Save Save

View File

@@ -10,6 +10,7 @@ import { z } from 'zod';
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments'; import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import CommentForm from '../ui/CommentForm'; import CommentForm from '../ui/CommentForm';
interface BeerCommentFormProps { interface BeerCommentFormProps {
@@ -31,20 +32,21 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async ( const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
data, data,
) => { ) => {
const loadingToast = toast.loading('Posting a new comment...');
try { try {
await sendCreateBeerCommentRequest({ await sendCreateBeerCommentRequest({
content: data.content, content: data.content,
rating: data.rating, rating: data.rating,
beerPostId: beerPost.id, beerPostId: beerPost.id,
}); });
await mutate();
reset(); reset();
toast.success('Created a new comment!'); toast.remove(loadingToast);
toast.success('Comment posted successfully.');
await mutate();
} catch (error) { } catch (error) {
const errorMessage = await mutate();
error instanceof Error ? error.message : 'Something went wrong.'; toast.remove(loadingToast);
toast.error(errorMessage); createErrorToast(error);
reset(); reset();
} }
}; };

View File

@@ -67,8 +67,8 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
{ {
/** /**
* If the comments are loading, show a loading component. Otherwise, show the * If the comments are loading, show a loading component. Otherwise, show
* comments. * the comments.
*/ */
isLoading ? ( isLoading ? (
<div className="card bg-base-300 pb-6"> <div className="card bg-base-300 pb-6">

View File

@@ -19,8 +19,8 @@ const BeerRecommendationsSection: FC<{
const { ref: penultimateBeerPostRef } = useInView({ const { ref: penultimateBeerPostRef } = useInView({
/** /**
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to * When the last beer post comes into view, call setSize from
* load more beer posts. * useBeerPostsByBrewery to load more beer posts.
*/ */
onChange: (visible) => { onChange: (visible) => {
if (!visible || isAtEnd) return; if (!visible || isAtEnd) return;
@@ -46,8 +46,9 @@ const BeerRecommendationsSection: FC<{
const isPenultimateBeerPost = index === beerPosts.length - 2; const isPenultimateBeerPost = index === beerPosts.length - 2;
/** /**
* Attach a ref to the second last beer post in the list. When it comes * Attach a ref to the second last beer post in the list.
* into view, the component will call setSize to load more beer posts. * When it comes into view, the component will call
* setSize to load more beer posts.
*/ */
return ( return (
@@ -85,8 +86,8 @@ const BeerRecommendationsSection: FC<{
{ {
/** /**
* If there are more beer posts to load, show a loading component with a * If there are more beer posts to load, show a loading component
* skeleton loader and a loading spinner. * with a skeleton loader and a loading spinner.
*/ */
!!isLoadingMore && !isAtEnd && ( !!isLoadingMore && !isAtEnd && (
<BeerRecommendationLoadingComponent length={PAGE_SIZE} /> <BeerRecommendationLoadingComponent length={PAGE_SIZE} />

View File

@@ -22,8 +22,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
}); });
const { ref: penultimateBeerPostRef } = useInView({ const { ref: penultimateBeerPostRef } = useInView({
/** /**
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to * When the last beer post comes into view, call setSize from
* load more beer posts. * useBeerPostsByBrewery to load more beer posts.
*/ */
onChange: (visible) => { onChange: (visible) => {
if (!visible || isAtEnd) return; if (!visible || isAtEnd) return;
@@ -60,8 +60,9 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
const isPenultimateBeerPost = index === beerPosts.length - 2; const isPenultimateBeerPost = index === beerPosts.length - 2;
/** /**
* Attach a ref to the second last beer post in the list. When it comes * Attach a ref to the second last beer post in the list.
* into view, the component will call setSize to load more beer posts. * When it comes into view, the component will call
* setSize to load more beer posts.
*/ */
return ( return (
@@ -90,8 +91,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
{ {
/** /**
* If there are more beer posts to load, show a loading component with a * If there are more beer posts to load, show a loading component
* skeleton loader and a loading spinner. * with a skeleton loader and a loading spinner.
*/ */
!!isLoadingMore && !isAtEnd && ( !!isLoadingMore && !isAtEnd && (
<BeerRecommendationLoadingComponent length={PAGE_SIZE} /> <BeerRecommendationLoadingComponent length={PAGE_SIZE} />

View File

@@ -0,0 +1,60 @@
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import { FC } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { z } from 'zod';
import sendCreateBreweryCommentRequest from '@/requests/BreweryComment/sendCreateBreweryCommentRequest';
import createErrorToast from '@/util/createErrorToast';
import CommentForm from '../ui/CommentForm';
interface BreweryCommentFormProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
mutate: ReturnType<typeof useBreweryPostComments>['mutate'];
}
const BreweryCommentForm: FC<BreweryCommentFormProps> = ({ breweryPost, mutate }) => {
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
z.infer<typeof CreateCommentValidationSchema>
>({
defaultValues: { rating: 0 },
resolver: zodResolver(CreateCommentValidationSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
data,
) => {
const loadingToast = toast.loading('Posting a new comment...');
try {
await sendCreateBreweryCommentRequest({
content: data.content,
rating: data.rating,
breweryPostId: breweryPost.id,
});
reset();
toast.remove(loadingToast);
toast.success('Comment posted successfully.');
await mutate();
} catch (error) {
await mutate();
toast.remove(loadingToast);
createErrorToast(error);
reset();
}
};
return (
<CommentForm
handleSubmit={handleSubmit}
onSubmit={onSubmit}
watch={watch}
setValue={setValue}
formState={formState}
register={register}
/>
);
};
export default BreweryCommentForm;

View File

@@ -3,92 +3,16 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
import { FC, MutableRefObject, useContext, useRef } from 'react'; import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler } from 'react-hook-form';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments'; import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
import toast from 'react-hot-toast';
import LoadingComponent from '../BeerById/LoadingComponent'; import LoadingComponent from '../BeerById/LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent'; import CommentsComponent from '../ui/CommentsComponent';
import CommentForm from '../ui/CommentForm'; import BreweryCommentForm from './BreweryCommentForm';
interface BreweryBeerSectionProps { interface BreweryBeerSectionProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>; breweryPost: z.infer<typeof BreweryPostQueryResult>;
} }
interface BreweryCommentFormProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
mutate: ReturnType<typeof useBreweryPostComments>['mutate'];
}
const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
breweryPostId: z.string(),
});
const sendCreateBreweryCommentRequest = async ({
content,
rating,
breweryPostId,
}: z.infer<typeof BreweryCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/breweries/${breweryPostId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) {
throw new Error('Invalid API response');
}
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
const BreweryCommentForm: FC<BreweryCommentFormProps> = ({ breweryPost, mutate }) => {
const { register, handleSubmit, formState, watch, reset, setValue } = useForm<
z.infer<typeof CreateCommentValidationSchema>
>({
defaultValues: { rating: 0 },
resolver: zodResolver(CreateCommentValidationSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
data,
) => {
await sendCreateBreweryCommentRequest({
content: data.content,
rating: data.rating,
breweryPostId: breweryPost.id,
});
await mutate();
toast.loading('Created new comment.');
reset();
};
return (
<CommentForm
handleSubmit={handleSubmit}
onSubmit={onSubmit}
watch={watch}
setValue={setValue}
formState={formState}
register={register}
/>
);
};
const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => { const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);

View File

@@ -32,15 +32,15 @@ const LoginForm = () => {
const { mutate } = useContext(UserContext); const { mutate } = useContext(UserContext);
const onSubmit: SubmitHandler<LoginT> = async (data) => { const onSubmit: SubmitHandler<LoginT> = async (data) => {
const id = toast.loading('Logging in.'); const loadingToast = toast.loading('Logging in...');
try { try {
await sendLoginUserRequest(data); await sendLoginUserRequest(data);
await mutate!(); await mutate!();
toast.remove(id); toast.remove(loadingToast);
toast.success('Logged in!'); toast.success('Logged in!');
await router.push(`/user/current`); await router.push(`/user/current`);
} catch (error) { } catch (error) {
toast.remove(id); toast.remove(loadingToast);
createErrorToast(error); createErrorToast(error);
reset(); reset();
} }

View File

@@ -68,8 +68,9 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
const isLastComment = index === comments.length - 1; const isLastComment = index === comments.length - 1;
/** /**
* Attach a ref to the last comment in the list. When it comes into view, the * Attach a ref to the last comment in the list. When it comes
* component will call setSize to load more comments. * into view, the component will call setSize to load more
* comments.
*/ */
return ( return (
<div <div
@@ -88,16 +89,17 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
{ {
/** /**
* If there are more comments to load, show a loading component with a * If there are more comments to load, show a loading component
* skeleton loader and a loading spinner. * with a skeleton loader and a loading spinner.
*/ */
!!isLoadingMore && <LoadingComponent length={pageSize} /> !!isLoadingMore && <LoadingComponent length={pageSize} />
} }
{ {
/** /**
* If the user has scrolled to the end of the comments, show a button that * If the user has scrolled to the end of the comments, show a
* will scroll them back to the top of the comments section. * button that will scroll them back to the top of the comments
* section.
*/ */
!!isAtEnd && ( !!isAtEnd && (
<div className="flex h-20 items-center justify-center text-center"> <div className="flex h-20 items-center justify-center text-center">

View File

@@ -22,7 +22,7 @@ const toastToClassName = (toastType: Toast['type']) => {
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => { const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
return ( return (
<> <>
<Toaster> <Toaster position="bottom-center">
{(t) => { {(t) => {
const alertType = toastToClassName(t.type); const alertType = toastToClassName(t.type);
return ( return (

View File

@@ -18,8 +18,9 @@ import { useState, useEffect } from 'react';
*/ */
const useMediaQuery = (query: `(${string})`) => { const useMediaQuery = (query: `(${string})`) => {
/** /**
* Initialize the matches state variable to false. This is updated whenever the viewport * Initialize the matches state variable to false. This is updated whenever the
* size changes (i.e. when the component is mounted and when the window is resized) * viewport size changes (i.e. when the component is mounted and when the window is
* resized)
*/ */
const [matches, setMatches] = useState(false); const [matches, setMatches] = useState(false);
@@ -34,8 +35,8 @@ const useMediaQuery = (query: `(${string})`) => {
} }
/** /**
* Add a resize event listener to the window object, and update the `matches` state * Add a resize event listener to the window object, and update the `matches`
* variable whenever the viewport size changes. * state variable whenever the viewport size changes.
*/ */
const listener = () => setMatches(media.matches); const listener = () => setMatches(media.matches);
window.addEventListener('resize', listener); window.addEventListener('resize', listener);

View File

@@ -3,16 +3,14 @@ import { NextPage } from 'next';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import Head from 'next/head'; import Head from 'next/head';
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import { z } from 'zod';
import DBClient from '@/prisma/DBClient';
import AccountInfo from '@/components/Account/AccountInfo'; import AccountInfo from '@/components/Account/AccountInfo';
import { useContext } from 'react';
import UserContext from '@/contexts/UserContext';
interface AccountPageProps { const AccountPage: NextPage = () => {
user: z.infer<typeof GetUserSchema>; const { user } = useContext(UserContext);
} if (!user) return null;
const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
return ( return (
<> <>
<Head> <Head>
@@ -30,7 +28,7 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
</div> </div>
<div className="flex flex-col items-center space-y-1"> <div className="flex flex-col items-center space-y-1">
<p className="text-3xl font-bold">Hello, {user.username}!</p> <p className="text-3xl font-bold">Hello, {user!.username}!</p>
<p className="text-lg">Welcome to your account page.</p> <p className="text-lg">Welcome to your account page.</p>
</div> </div>
</div> </div>
@@ -50,7 +48,7 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel> <Tab.Panel>
<AccountInfo user={user} /> <AccountInfo />
</Tab.Panel> </Tab.Panel>
<Tab.Panel>Content 3</Tab.Panel> <Tab.Panel>Content 3</Tab.Panel>
</Tab.Panels> </Tab.Panels>
@@ -64,30 +62,4 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
export default AccountPage; export default AccountPage;
export const getServerSideProps = withPageAuthRequired(async (context, session) => { export const getServerSideProps = withPageAuthRequired();
const { id } = session;
const user: z.infer<typeof GetUserSchema> | null =
await DBClient.instance.user.findUnique({
where: { id },
select: {
username: true,
email: true,
accountIsVerified: true,
firstName: true,
lastName: true,
dateOfBirth: true,
id: true,
createdAt: true,
},
});
if (!user) {
return { redirect: { destination: '/login', permanent: false } };
}
return {
props: {
user: JSON.parse(JSON.stringify(user)),
},
};
});

View File

@@ -0,0 +1,39 @@
import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult';
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
breweryPostId: z.string(),
});
const sendCreateBreweryCommentRequest = async ({
content,
rating,
breweryPostId,
}: z.infer<typeof BreweryCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/breweries/${breweryPostId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) {
throw new Error('Invalid API response');
}
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
export default sendCreateBreweryCommentRequest;

View File

@@ -0,0 +1,35 @@
import GetUserSchema from '@/services/User/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
interface SendEditUserRequestArgs {
user: z.infer<typeof GetUserSchema>;
data: {
username: string;
email: string;
firstName: string;
lastName: string;
};
}
const sendEditUserRequest = async ({ user, data }: SendEditUserRequestArgs) => {
const response = await fetch(`/api/users/${user!.id}/edit`, {
body: JSON.stringify(data),
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
});
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;
};
export default sendEditUserRequest;