mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
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:
@@ -1,24 +1,23 @@
|
||||
import validateEmailRequest from '@/requests/User/validateEmailRequest';
|
||||
import validateUsernameRequest from '@/requests/validateUsernameRequest';
|
||||
import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useRouter } from 'next/router';
|
||||
import { FC, useState } from 'react';
|
||||
import { FC, useContext, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormTextInput from '../ui/forms/FormTextInput';
|
||||
|
||||
interface AccountInfoProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
const AccountInfo: FC = () => {
|
||||
const { user, mutate } = useContext(UserContext);
|
||||
|
||||
const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
|
||||
const router = useRouter();
|
||||
const EditUserSchema = BaseCreateUserSchema.pick({
|
||||
username: true,
|
||||
email: true,
|
||||
@@ -30,7 +29,7 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
|
||||
.email({ message: 'Email must be a valid email address.' })
|
||||
.refine(
|
||||
async (email) => {
|
||||
if (user.email === email) return true;
|
||||
if (user!.email === email) return true;
|
||||
return validateEmailRequest(email);
|
||||
},
|
||||
{ message: 'Email is already taken.' },
|
||||
@@ -41,7 +40,7 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
|
||||
.max(20, { message: 'Username must be less than 20 characters.' })
|
||||
.refine(
|
||||
async (username) => {
|
||||
if (user.username === username) return true;
|
||||
if (user!.username === username) return true;
|
||||
return validateUsernameRequest(username);
|
||||
},
|
||||
{ message: 'Username is already taken.' },
|
||||
@@ -53,29 +52,29 @@ const AccountInfo: FC<AccountInfoProps> = ({ user }) => {
|
||||
>({
|
||||
resolver: zodResolver(EditUserSchema),
|
||||
defaultValues: {
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
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 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('Something went wrong.');
|
||||
const loadingToast = toast.loading('Submitting edits...');
|
||||
try {
|
||||
await sendEditUserRequest({ user: user!, data });
|
||||
await mutate!();
|
||||
setInEditMode(false);
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Edits submitted successfully.');
|
||||
} catch (error) {
|
||||
setInEditMode(false);
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
await mutate!();
|
||||
}
|
||||
|
||||
await response.json();
|
||||
|
||||
router.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,8 @@ import UserContext from '@/contexts/UserContext';
|
||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||
import { format } from 'date-fns';
|
||||
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 { z } from 'zod';
|
||||
|
||||
@@ -8,6 +8,7 @@ import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResul
|
||||
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
@@ -31,7 +32,6 @@ interface EditCommentBodyProps {
|
||||
const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
comment,
|
||||
setInEditMode,
|
||||
|
||||
mutate,
|
||||
handleDeleteRequest,
|
||||
handleEditRequest,
|
||||
@@ -43,24 +43,41 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
resolver: zodResolver(CreateCommentValidationSchema),
|
||||
});
|
||||
|
||||
const { errors } = formState;
|
||||
const { errors, isSubmitting } = formState;
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const onDelete = async () => {
|
||||
const loadingToast = toast.loading('Deleting comment...');
|
||||
setIsDeleting(true);
|
||||
await handleDeleteRequest(comment.id);
|
||||
await mutate();
|
||||
try {
|
||||
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 (
|
||||
data,
|
||||
) => {
|
||||
setInEditMode(true);
|
||||
await handleEditRequest(comment.id, data);
|
||||
await mutate();
|
||||
toast.success('Submitted edits');
|
||||
setInEditMode(false);
|
||||
const loadingToast = toast.loading('Submitting comment edits...');
|
||||
|
||||
try {
|
||||
setInEditMode(true);
|
||||
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 (
|
||||
@@ -78,7 +95,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
placeholder="Comment"
|
||||
rows={2}
|
||||
error={!!errors.content?.message}
|
||||
disabled={formState.isSubmitting || isDeleting}
|
||||
disabled={isSubmitting || isDeleting}
|
||||
/>
|
||||
</FormSegment>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -97,8 +114,8 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
<Rating.Item
|
||||
name="rating-1"
|
||||
className="mask mask-star cursor-default"
|
||||
disabled={formState.isSubmitting || isDeleting}
|
||||
aria-disabled={formState.isSubmitting || isDeleting}
|
||||
disabled={isSubmitting || isDeleting}
|
||||
aria-disabled={isSubmitting || isDeleting}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
@@ -108,7 +125,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="btn-xs btn lg:btn-sm"
|
||||
disabled={formState.isSubmitting || isDeleting}
|
||||
disabled={isSubmitting || isDeleting}
|
||||
onClick={() => {
|
||||
setInEditMode(false);
|
||||
}}
|
||||
@@ -117,7 +134,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formState.isSubmitting || isDeleting}
|
||||
disabled={isSubmitting || isDeleting}
|
||||
className="btn-xs btn lg:btn-sm"
|
||||
>
|
||||
Save
|
||||
|
||||
@@ -10,6 +10,7 @@ import { z } from 'zod';
|
||||
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
|
||||
import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import CommentForm from '../ui/CommentForm';
|
||||
|
||||
interface BeerCommentFormProps {
|
||||
@@ -31,20 +32,21 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
|
||||
const onSubmit: SubmitHandler<z.infer<typeof CreateCommentValidationSchema>> = async (
|
||||
data,
|
||||
) => {
|
||||
const loadingToast = toast.loading('Posting a new comment...');
|
||||
try {
|
||||
await sendCreateBeerCommentRequest({
|
||||
content: data.content,
|
||||
rating: data.rating,
|
||||
beerPostId: beerPost.id,
|
||||
});
|
||||
await mutate();
|
||||
reset();
|
||||
toast.success('Created a new comment!');
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Comment posted successfully.');
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Something went wrong.';
|
||||
toast.error(errorMessage);
|
||||
|
||||
await mutate();
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,8 +67,8 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
|
||||
|
||||
{
|
||||
/**
|
||||
* If the comments are loading, show a loading component. Otherwise, show the
|
||||
* comments.
|
||||
* If the comments are loading, show a loading component. Otherwise, show
|
||||
* the comments.
|
||||
*/
|
||||
isLoading ? (
|
||||
<div className="card bg-base-300 pb-6">
|
||||
|
||||
@@ -19,8 +19,8 @@ const BeerRecommendationsSection: FC<{
|
||||
|
||||
const { ref: penultimateBeerPostRef } = useInView({
|
||||
/**
|
||||
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||
* load more beer posts.
|
||||
* When the last beer post comes into view, call setSize from
|
||||
* useBeerPostsByBrewery to load more beer posts.
|
||||
*/
|
||||
onChange: (visible) => {
|
||||
if (!visible || isAtEnd) return;
|
||||
@@ -46,8 +46,9 @@ const BeerRecommendationsSection: FC<{
|
||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||
|
||||
/**
|
||||
* Attach a ref to the second last beer post in the list. When it comes
|
||||
* into view, the component will call setSize to load more beer posts.
|
||||
* Attach a ref to the second last beer post in the list.
|
||||
* When it comes into view, the component will call
|
||||
* setSize to load more beer posts.
|
||||
*/
|
||||
|
||||
return (
|
||||
@@ -85,8 +86,8 @@ const BeerRecommendationsSection: FC<{
|
||||
|
||||
{
|
||||
/**
|
||||
* If there are more beer posts to load, show a loading component with a
|
||||
* skeleton loader and a loading spinner.
|
||||
* If there are more beer posts to load, show a loading component
|
||||
* with a skeleton loader and a loading spinner.
|
||||
*/
|
||||
!!isLoadingMore && !isAtEnd && (
|
||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||
|
||||
@@ -22,8 +22,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
||||
});
|
||||
const { ref: penultimateBeerPostRef } = useInView({
|
||||
/**
|
||||
* When the last beer post comes into view, call setSize from useBeerPostsByBrewery to
|
||||
* load more beer posts.
|
||||
* When the last beer post comes into view, call setSize from
|
||||
* useBeerPostsByBrewery to load more beer posts.
|
||||
*/
|
||||
onChange: (visible) => {
|
||||
if (!visible || isAtEnd) return;
|
||||
@@ -60,8 +60,9 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
||||
const isPenultimateBeerPost = index === beerPosts.length - 2;
|
||||
|
||||
/**
|
||||
* Attach a ref to the second last beer post in the list. When it comes
|
||||
* into view, the component will call setSize to load more beer posts.
|
||||
* Attach a ref to the second last beer post in the list.
|
||||
* When it comes into view, the component will call
|
||||
* setSize to load more beer posts.
|
||||
*/
|
||||
|
||||
return (
|
||||
@@ -90,8 +91,8 @@ const BreweryBeersSection: FC<BreweryCommentsSectionProps> = ({ breweryPost }) =
|
||||
|
||||
{
|
||||
/**
|
||||
* If there are more beer posts to load, show a loading component with a
|
||||
* skeleton loader and a loading spinner.
|
||||
* If there are more beer posts to load, show a loading component
|
||||
* with a skeleton loader and a loading spinner.
|
||||
*/
|
||||
!!isLoadingMore && !isAtEnd && (
|
||||
<BeerRecommendationLoadingComponent length={PAGE_SIZE} />
|
||||
|
||||
60
src/components/BreweryById/BreweryCommentForm.tsx
Normal file
60
src/components/BreweryById/BreweryCommentForm.tsx
Normal 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;
|
||||
@@ -3,92 +3,16 @@ import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQuer
|
||||
import { FC, MutableRefObject, useContext, useRef } from 'react';
|
||||
import { z } from 'zod';
|
||||
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 toast from 'react-hot-toast';
|
||||
import LoadingComponent from '../BeerById/LoadingComponent';
|
||||
import CommentsComponent from '../ui/CommentsComponent';
|
||||
import CommentForm from '../ui/CommentForm';
|
||||
import BreweryCommentForm from './BreweryCommentForm';
|
||||
|
||||
interface BreweryBeerSectionProps {
|
||||
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 { user } = useContext(UserContext);
|
||||
|
||||
|
||||
@@ -32,15 +32,15 @@ const LoginForm = () => {
|
||||
const { mutate } = useContext(UserContext);
|
||||
|
||||
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
||||
const id = toast.loading('Logging in.');
|
||||
const loadingToast = toast.loading('Logging in...');
|
||||
try {
|
||||
await sendLoginUserRequest(data);
|
||||
await mutate!();
|
||||
toast.remove(id);
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Logged in!');
|
||||
await router.push(`/user/current`);
|
||||
} catch (error) {
|
||||
toast.remove(id);
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
reset();
|
||||
}
|
||||
|
||||
@@ -68,8 +68,9 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
||||
const isLastComment = index === comments.length - 1;
|
||||
|
||||
/**
|
||||
* Attach a ref to the last comment in the list. When it comes into view, the
|
||||
* component will call setSize to load more comments.
|
||||
* Attach a ref to the last comment in the list. When it comes
|
||||
* into view, the component will call setSize to load more
|
||||
* comments.
|
||||
*/
|
||||
return (
|
||||
<div
|
||||
@@ -88,16 +89,17 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
|
||||
|
||||
{
|
||||
/**
|
||||
* If there are more comments to load, show a loading component with a
|
||||
* skeleton loader and a loading spinner.
|
||||
* If there are more comments to load, show a loading component
|
||||
* with a skeleton loader and a loading spinner.
|
||||
*/
|
||||
!!isLoadingMore && <LoadingComponent length={pageSize} />
|
||||
}
|
||||
|
||||
{
|
||||
/**
|
||||
* If the user has scrolled to the end of the comments, show a button that
|
||||
* will scroll them back to the top of the comments section.
|
||||
* If the user has scrolled to the end of the comments, show a
|
||||
* button that will scroll them back to the top of the comments
|
||||
* section.
|
||||
*/
|
||||
!!isAtEnd && (
|
||||
<div className="flex h-20 items-center justify-center text-center">
|
||||
|
||||
@@ -22,7 +22,7 @@ const toastToClassName = (toastType: Toast['type']) => {
|
||||
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Toaster>
|
||||
<Toaster position="bottom-center">
|
||||
{(t) => {
|
||||
const alertType = toastToClassName(t.type);
|
||||
return (
|
||||
|
||||
@@ -18,8 +18,9 @@ import { useState, useEffect } from 'react';
|
||||
*/
|
||||
const useMediaQuery = (query: `(${string})`) => {
|
||||
/**
|
||||
* Initialize the matches state variable to false. This is updated whenever the viewport
|
||||
* size changes (i.e. when the component is mounted and when the window is resized)
|
||||
* Initialize the matches state variable to false. This is updated whenever the
|
||||
* viewport size changes (i.e. when the component is mounted and when the window is
|
||||
* resized)
|
||||
*/
|
||||
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
|
||||
* variable whenever the viewport size changes.
|
||||
* Add a resize event listener to the window object, and update the `matches`
|
||||
* state variable whenever the viewport size changes.
|
||||
*/
|
||||
const listener = () => setMatches(media.matches);
|
||||
window.addEventListener('resize', listener);
|
||||
|
||||
@@ -3,16 +3,14 @@ import { NextPage } from 'next';
|
||||
|
||||
import { Tab } from '@headlessui/react';
|
||||
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 { useContext } from 'react';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
|
||||
interface AccountPageProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
const AccountPage: NextPage = () => {
|
||||
const { user } = useContext(UserContext);
|
||||
if (!user) return null;
|
||||
|
||||
const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -30,7 +28,7 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +48,7 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<AccountInfo user={user} />
|
||||
<AccountInfo />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Content 3</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
@@ -64,30 +62,4 @@ const AccountPage: NextPage<AccountPageProps> = ({ user }) => {
|
||||
|
||||
export default AccountPage;
|
||||
|
||||
export const getServerSideProps = withPageAuthRequired(async (context, session) => {
|
||||
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)),
|
||||
},
|
||||
};
|
||||
});
|
||||
export const getServerSideProps = withPageAuthRequired();
|
||||
|
||||
@@ -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;
|
||||
35
src/requests/User/sendEditUserRequest.ts
Normal file
35
src/requests/User/sendEditUserRequest.ts
Normal 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;
|
||||
Reference in New Issue
Block a user