mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 18:52:06 +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 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 (
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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);
|
||||||
|
try {
|
||||||
await handleDeleteRequest(comment.id);
|
await handleDeleteRequest(comment.id);
|
||||||
await mutate();
|
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,
|
||||||
) => {
|
) => {
|
||||||
|
const loadingToast = toast.loading('Submitting comment edits...');
|
||||||
|
|
||||||
|
try {
|
||||||
setInEditMode(true);
|
setInEditMode(true);
|
||||||
await handleEditRequest(comment.id, data);
|
await handleEditRequest(comment.id, data);
|
||||||
await mutate();
|
await mutate();
|
||||||
toast.success('Submitted edits');
|
toast.remove(loadingToast);
|
||||||
|
toast.success('Comment edits submitted successfully.');
|
||||||
setInEditMode(false);
|
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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
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 { 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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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