mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,3 +38,6 @@ next-env.d.ts
|
|||||||
|
|
||||||
# http requests
|
# http requests
|
||||||
*.http
|
*.http
|
||||||
|
|
||||||
|
# uploaded images
|
||||||
|
public/uploads
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
import sendCreateBeerCommentRequest from '@/requests/sendCreateBeerCommentRequest';
|
||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Dispatch, SetStateAction, FunctionComponent, useState, useEffect } from 'react';
|
import { Dispatch, SetStateAction, FunctionComponent, useState, useEffect } from 'react';
|
||||||
@@ -26,7 +26,6 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
|
|||||||
z.infer<typeof BeerCommentValidationSchema>
|
z.infer<typeof BeerCommentValidationSchema>
|
||||||
>({
|
>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
beerPostId: beerPost.id,
|
|
||||||
rating: 0,
|
rating: 0,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(BeerCommentValidationSchema),
|
resolver: zodResolver(BeerCommentValidationSchema),
|
||||||
@@ -35,8 +34,8 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
|
|||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRating(0);
|
setRating(0);
|
||||||
reset({ beerPostId: beerPost.id, rating: 0, content: '' });
|
reset({ rating: 0, content: '' });
|
||||||
}, [beerPost.id, reset]);
|
}, [reset]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
|
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
|
||||||
@@ -44,7 +43,11 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({ beerPost })
|
|||||||
) => {
|
) => {
|
||||||
setValue('rating', 0);
|
setValue('rating', 0);
|
||||||
setRating(0);
|
setRating(0);
|
||||||
await sendCreateBeerCommentRequest(data);
|
await sendCreateBeerCommentRequest({
|
||||||
|
content: data.content,
|
||||||
|
rating: data.rating,
|
||||||
|
beerPostId: beerPost.id,
|
||||||
|
});
|
||||||
reset();
|
reset();
|
||||||
router.replace(router.asPath, undefined, { scroll: false });
|
router.replace(router.asPath, undefined, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,50 +2,29 @@ import Link from 'next/link';
|
|||||||
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
|
import formatDistanceStrict from 'date-fns/formatDistanceStrict';
|
||||||
import format from 'date-fns/format';
|
import format from 'date-fns/format';
|
||||||
import { FC, useContext, useEffect, useState } from 'react';
|
import { FC, useContext, useEffect, useState } from 'react';
|
||||||
import { FaRegThumbsUp, FaThumbsUp } from 'react-icons/fa';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
|
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest';
|
import BeerPostLikeButton from './BeerPostLikeButton';
|
||||||
import sendLikeRequest from '../../requests/sendLikeRequest';
|
|
||||||
|
|
||||||
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) => {
|
const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult; initialLikeCount: number }> = ({
|
||||||
|
beerPost,
|
||||||
|
initialLikeCount,
|
||||||
|
}) => {
|
||||||
const createdAtDate = new Date(beerPost.createdAt);
|
const createdAtDate = new Date(beerPost.createdAt);
|
||||||
const [timeDistance, setTimeDistance] = useState('');
|
const [timeDistance, setTimeDistance] = useState('');
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [likeCount, setLikeCount] = useState(initialLikeCount);
|
||||||
const [isLiked, setIsLiked] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) {
|
setLikeCount(initialLikeCount);
|
||||||
setLoading(false);
|
}, [initialLikeCount]);
|
||||||
return;
|
|
||||||
}
|
|
||||||
sendCheckIfUserLikesBeerPostRequest(beerPost.id)
|
|
||||||
.then((currentLikeStatus) => {
|
|
||||||
setIsLiked(currentLikeStatus);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, [user, beerPost.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeDistance(formatDistanceStrict(new Date(beerPost.createdAt), new Date()));
|
setTimeDistance(formatDistanceStrict(new Date(beerPost.createdAt), new Date()));
|
||||||
}, [beerPost.createdAt]);
|
}, [beerPost.createdAt]);
|
||||||
|
|
||||||
const handleLike = async () => {
|
|
||||||
try {
|
|
||||||
await sendLikeRequest(beerPost);
|
|
||||||
setIsLiked(!isLiked);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card flex flex-col justify-center bg-base-300">
|
<div className="card flex flex-col justify-center bg-base-300">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@@ -75,8 +54,8 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) =>
|
|||||||
|
|
||||||
<p>{beerPost.description}</p>
|
<p>{beerPost.description}</p>
|
||||||
<div className="mt-5 flex justify-between">
|
<div className="mt-5 flex justify-between">
|
||||||
<div>
|
<div className="space-y-1">
|
||||||
<div className="mb-1">
|
<div>
|
||||||
<Link
|
<Link
|
||||||
className="link-hover link text-lg font-bold"
|
className="link-hover link text-lg font-bold"
|
||||||
href={`/beers/types/${beerPost.type.id}`}
|
href={`/beers/types/${beerPost.type.id}`}
|
||||||
@@ -88,31 +67,15 @@ const BeerInfoHeader: FC<{ beerPost: BeerPostQueryResult }> = ({ beerPost }) =>
|
|||||||
<span className="mr-4 text-lg font-medium">{beerPost.abv}% ABV</span>
|
<span className="mr-4 text-lg font-medium">{beerPost.abv}% ABV</span>
|
||||||
<span className="text-lg font-medium">{beerPost.ibu} IBU</span>
|
<span className="text-lg font-medium">{beerPost.ibu} IBU</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
Liked by {likeCount} user{likeCount !== 1 && 's'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-actions">
|
<div className="card-actions items-end">
|
||||||
{user && (
|
{user && (
|
||||||
<button
|
<BeerPostLikeButton beerPostId={beerPost.id} setLikeCount={setLikeCount} />
|
||||||
type="button"
|
|
||||||
className={`btn gap-2 rounded-2xl ${
|
|
||||||
!isLiked ? 'btn-ghost outline' : 'btn-primary'
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
handleLike();
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{isLiked ? (
|
|
||||||
<>
|
|
||||||
<FaThumbsUp className="text-2xl" />
|
|
||||||
<span>Liked</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<FaRegThumbsUp className="text-2xl" />
|
|
||||||
<span>Like</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
69
components/BeerById/BeerPostLikeButton.tsx
Normal file
69
components/BeerById/BeerPostLikeButton.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import sendLikeRequest from '@/requests/sendLikeRequest';
|
||||||
|
import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react';
|
||||||
|
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
|
||||||
|
import sendCheckIfUserLikesBeerPostRequest from '@/requests/sendCheckIfUserLikesBeerPostRequest';
|
||||||
|
|
||||||
|
const BeerPostLikeButton: FC<{
|
||||||
|
beerPostId: string;
|
||||||
|
setLikeCount: Dispatch<SetStateAction<number>>;
|
||||||
|
}> = ({ beerPostId, setLikeCount }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isLiked, setIsLiked] = useState(false);
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendCheckIfUserLikesBeerPostRequest(beerPostId)
|
||||||
|
.then((currentLikeStatus) => {
|
||||||
|
setIsLiked(currentLikeStatus);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [user, beerPostId]);
|
||||||
|
|
||||||
|
const handleLike = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await sendLikeRequest(beerPostId);
|
||||||
|
setIsLiked(!isLiked);
|
||||||
|
setLikeCount((prevCount) => prevCount + (isLiked ? -1 : 1));
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn gap-2 rounded-2xl ${
|
||||||
|
!isLiked ? 'btn-ghost outline' : 'btn-primary'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
handleLike();
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{isLiked ? (
|
||||||
|
<>
|
||||||
|
<FaThumbsUp className="text-2xl" />
|
||||||
|
Liked
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaRegThumbsUp className="text-2xl" />
|
||||||
|
Like
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BeerPostLikeButton;
|
||||||
@@ -13,7 +13,7 @@ const CommentCard: React.FC<{
|
|||||||
}, [comment.createdAt]);
|
}, [comment.createdAt]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card-body h-[1/9]">
|
<div className="card-body h-64">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold">{comment.postedBy.username}</h3>
|
<h3 className="text-2xl font-semibold">{comment.postedBy.username}</h3>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
|
import sendCreateBeerPostRequest from '@/requests/sendCreateBeerPostRequest';
|
||||||
import BeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { BeerType } from '@prisma/client';
|
import { BeerType } from '@prisma/client';
|
||||||
import router from 'next/router';
|
import router from 'next/router';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import ErrorAlert from './ui/alerts/ErrorAlert';
|
||||||
import Button from './ui/forms/Button';
|
import Button from './ui/forms/Button';
|
||||||
import FormError from './ui/forms/FormError';
|
import FormError from './ui/forms/FormError';
|
||||||
import FormInfo from './ui/forms/FormInfo';
|
import FormInfo from './ui/forms/FormInfo';
|
||||||
@@ -16,7 +17,7 @@ import FormSelect from './ui/forms/FormSelect';
|
|||||||
import FormTextArea from './ui/forms/FormTextArea';
|
import FormTextArea from './ui/forms/FormTextArea';
|
||||||
import FormTextInput from './ui/forms/FormTextInput';
|
import FormTextInput from './ui/forms/FormTextInput';
|
||||||
|
|
||||||
type BeerPostT = z.infer<typeof BeerPostValidationSchema>;
|
type BeerPostT = z.infer<typeof CreateBeerPostValidationSchema>;
|
||||||
|
|
||||||
interface BeerFormProps {
|
interface BeerFormProps {
|
||||||
formType: 'edit' | 'create';
|
formType: 'edit' | 'create';
|
||||||
@@ -37,7 +38,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<BeerPostT>({
|
} = useForm<BeerPostT>({
|
||||||
resolver: zodResolver(BeerPostValidationSchema),
|
resolver: zodResolver(CreateBeerPostValidationSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: defaultValues?.name,
|
name: defaultValues?.name,
|
||||||
description: defaultValues?.description,
|
description: defaultValues?.description,
|
||||||
@@ -46,7 +47,12 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<BeerPostT> = async (data) => {
|
const onSubmit: SubmitHandler<BeerPostT> = async (data) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
switch (formType) {
|
switch (formType) {
|
||||||
case 'create': {
|
case 'create': {
|
||||||
try {
|
try {
|
||||||
@@ -54,8 +60,9 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
router.push(`/beers/${response.id}`);
|
router.push(`/beers/${response.id}`);
|
||||||
break;
|
break;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// eslint-disable-next-line no-console
|
if (e instanceof Error) {
|
||||||
console.error(e);
|
setError(e.message);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +75,9 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="form-control" onSubmit={handleSubmit(onSubmit)}>
|
<form className="form-control" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="my-5">
|
||||||
|
{error && <ErrorAlert error={error} setError={setError} />}
|
||||||
|
</div>
|
||||||
<FormInfo>
|
<FormInfo>
|
||||||
<FormLabel htmlFor="name">Name</FormLabel>
|
<FormLabel htmlFor="name">Name</FormLabel>
|
||||||
<FormError>{errors.name?.message}</FormError>
|
<FormError>{errors.name?.message}</FormError>
|
||||||
@@ -79,6 +89,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
error={!!errors.name}
|
error={!!errors.name}
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
</FormSegment>
|
</FormSegment>
|
||||||
{formType === 'create' && breweries.length && (
|
{formType === 'create' && breweries.length && (
|
||||||
@@ -89,6 +100,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormSegment>
|
<FormSegment>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
|
disabled={isSubmitting}
|
||||||
formRegister={register('breweryId')}
|
formRegister={register('breweryId')}
|
||||||
error={!!errors.breweryId}
|
error={!!errors.breweryId}
|
||||||
id="breweryId"
|
id="breweryId"
|
||||||
@@ -110,6 +122,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
<FormError>{errors.abv?.message}</FormError>
|
<FormError>{errors.abv?.message}</FormError>
|
||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
|
disabled={isSubmitting}
|
||||||
placeholder="12"
|
placeholder="12"
|
||||||
formValidationSchema={register('abv', { valueAsNumber: true })}
|
formValidationSchema={register('abv', { valueAsNumber: true })}
|
||||||
error={!!errors.abv}
|
error={!!errors.abv}
|
||||||
@@ -123,6 +136,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
<FormError>{errors.ibu?.message}</FormError>
|
<FormError>{errors.ibu?.message}</FormError>
|
||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
|
disabled={isSubmitting}
|
||||||
placeholder="52"
|
placeholder="52"
|
||||||
formValidationSchema={register('ibu', { valueAsNumber: true })}
|
formValidationSchema={register('ibu', { valueAsNumber: true })}
|
||||||
error={!!errors.ibu}
|
error={!!errors.ibu}
|
||||||
@@ -138,6 +152,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormSegment>
|
<FormSegment>
|
||||||
<FormTextArea
|
<FormTextArea
|
||||||
|
disabled={isSubmitting}
|
||||||
placeholder="Ratione cumque quas quia aut impedit ea culpa facere. Ut in sit et quas reiciendis itaque."
|
placeholder="Ratione cumque quas quia aut impedit ea culpa facere. Ut in sit et quas reiciendis itaque."
|
||||||
error={!!errors.description}
|
error={!!errors.description}
|
||||||
formValidationSchema={register('description')}
|
formValidationSchema={register('description')}
|
||||||
@@ -152,6 +167,7 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormSegment>
|
<FormSegment>
|
||||||
<FormSelect
|
<FormSelect
|
||||||
|
disabled={isSubmitting}
|
||||||
formRegister={register('typeId')}
|
formRegister={register('typeId')}
|
||||||
error={!!errors.typeId}
|
error={!!errors.typeId}
|
||||||
id="typeId"
|
id="typeId"
|
||||||
@@ -164,11 +180,19 @@ const BeerForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
</FormSegment>
|
</FormSegment>
|
||||||
|
|
||||||
<Button type="submit">{`${
|
{!isSubmitting && (
|
||||||
formType === 'edit'
|
<Button type="submit" isSubmitting={isSubmitting}>{`${
|
||||||
? `Edit ${defaultValues?.name || 'beer post'}`
|
formType === 'edit'
|
||||||
: 'Create beer post'
|
? `Edit ${defaultValues?.name || 'beer post'}`
|
||||||
} `}</Button>
|
: 'Create beer post'
|
||||||
|
}`}</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSubmitting && (
|
||||||
|
<Button type="submit" isSubmitting={isSubmitting}>
|
||||||
|
Submitting
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => {
|
const BeerCard: FC<{ post: BeerPostQueryResult }> = ({ post }) => {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-300" key={post.id}>
|
<div className="card bg-base-300" key={post.id}>
|
||||||
<figure className="card-image h-96">
|
<figure className="card-image h-96">
|
||||||
{post.beerImages.length > 0 && (
|
{post.beerImages.length > 0 && (
|
||||||
<Image src={post.beerImages[0].url} alt={post.name} width="1029" height="110" />
|
<Image
|
||||||
|
src={post.beerImages[0].path}
|
||||||
|
alt={post.name}
|
||||||
|
width="1029"
|
||||||
|
height="110"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import sendLoginUserRequest from '@/requests/sendLoginUserRequest';
|
|||||||
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import ErrorAlert from '../ui/alerts/ErrorAlert';
|
||||||
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';
|
||||||
@@ -13,7 +15,7 @@ import FormTextInput from '../ui/forms/FormTextInput';
|
|||||||
type LoginT = z.infer<typeof LoginValidationSchema>;
|
type LoginT = z.infer<typeof LoginValidationSchema>;
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { register, handleSubmit, formState } = useForm<LoginT>({
|
const { register, handleSubmit, formState, reset } = useForm<LoginT>({
|
||||||
resolver: zodResolver(LoginValidationSchema),
|
resolver: zodResolver(LoginValidationSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -23,18 +25,23 @@ const LoginForm = () => {
|
|||||||
|
|
||||||
const { errors } = formState;
|
const { errors } = formState;
|
||||||
|
|
||||||
|
const [responseError, setResponseError] = useState<string>('');
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
const onSubmit: SubmitHandler<LoginT> = async (data) => {
|
||||||
try {
|
try {
|
||||||
const response = await sendLoginUserRequest(data);
|
const response = await sendLoginUserRequest(data);
|
||||||
|
|
||||||
router.push(`/users/${response.id}`);
|
router.push(`/users/${response.id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
if (error instanceof Error) {
|
||||||
|
setResponseError(error.message);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="form-control w-9/12 space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
<form className="form-control w-full space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<div>
|
<div>
|
||||||
<FormInfo>
|
<FormInfo>
|
||||||
<FormLabel htmlFor="username">username</FormLabel>
|
<FormLabel htmlFor="username">username</FormLabel>
|
||||||
@@ -65,8 +72,9 @@ const LoginForm = () => {
|
|||||||
</FormSegment>
|
</FormSegment>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{responseError && <ErrorAlert error={responseError} setError={setResponseError} />}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<button type="submit" className="btn btn-primary w-full">
|
<button type="submit" className="btn-primary btn w-full">
|
||||||
Login
|
Login
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
/* eslint-disable jsx-a11y/label-has-for */
|
/* eslint-disable jsx-a11y/label-has-for */
|
||||||
|
|
||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface Page {
|
interface Page {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -14,19 +15,36 @@ const Navbar = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [currentURL, setCurrentURL] = useState('/');
|
const [currentURL, setCurrentURL] = useState('/');
|
||||||
|
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentURL(router.asPath);
|
setCurrentURL(router.asPath);
|
||||||
}, [router.asPath]);
|
}, [router.asPath]);
|
||||||
|
|
||||||
const pages: Page[] = [
|
const authenticatedPages: readonly Page[] = [
|
||||||
|
{ slug: '/account', name: 'Account' },
|
||||||
|
{ slug: '/api/users/logout', name: 'Logout' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const unauthenticatedPages: readonly Page[] = [
|
||||||
|
{ slug: '/login', name: 'Login' },
|
||||||
|
{ slug: '/register', name: 'Register' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const otherPages: readonly Page[] = [
|
||||||
{ slug: '/beers', name: 'Beers' },
|
{ slug: '/beers', name: 'Beers' },
|
||||||
{ slug: '/breweries', name: 'Breweries' },
|
{ slug: '/breweries', name: 'Breweries' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pages: readonly Page[] = [
|
||||||
|
...otherPages,
|
||||||
|
...(user ? authenticatedPages : unauthenticatedPages),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar bg-primary">
|
<nav className="navbar bg-primary text-primary-content">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Link className="btn btn-ghost text-3xl normal-case" href="/">
|
<Link className="btn-ghost btn text-3xl normal-case" href="/">
|
||||||
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
<span className="cursor-pointer text-xl font-bold">The Biergarten App</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,7 +57,7 @@ const Navbar = () => {
|
|||||||
<span
|
<span
|
||||||
className={`text-lg uppercase ${
|
className={`text-lg uppercase ${
|
||||||
currentURL === page.slug ? 'font-extrabold' : 'font-semibold'
|
currentURL === page.slug ? 'font-extrabold' : 'font-semibold'
|
||||||
} text-base-content`}
|
} text-primary-content`}
|
||||||
>
|
>
|
||||||
{page.name}
|
{page.name}
|
||||||
</span>
|
</span>
|
||||||
@@ -51,7 +69,7 @@ const Navbar = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-none lg:hidden">
|
<div className="flex-none lg:hidden">
|
||||||
<div className="dropdown-end dropdown">
|
<div className="dropdown-end dropdown">
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle">
|
<label tabIndex={0} className="btn-ghost btn-circle btn">
|
||||||
<span className="w-10 rounded-full">
|
<span className="w-10 rounded-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -75,7 +93,7 @@ const Navbar = () => {
|
|||||||
{pages.map((page) => (
|
{pages.map((page) => (
|
||||||
<li key={page.slug}>
|
<li key={page.slug}>
|
||||||
<Link href={page.slug}>
|
<Link href={page.slug}>
|
||||||
<span className="select-none">{page.name}</span>
|
<span className="select-none text-primary-content">{page.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
32
components/ui/alerts/ErrorAlert.tsx
Normal file
32
components/ui/alerts/ErrorAlert.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Dispatch, FC, SetStateAction } from 'react';
|
||||||
|
import { FiAlertTriangle } from 'react-icons/fi';
|
||||||
|
|
||||||
|
interface ErrorAlertProps {
|
||||||
|
error: string;
|
||||||
|
setError: Dispatch<SetStateAction<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
|
||||||
|
return (
|
||||||
|
<div className="alert alert-error shadow-lg">
|
||||||
|
<div>
|
||||||
|
<FiAlertTriangle className="h-6 w-6" />
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none">
|
||||||
|
<button
|
||||||
|
className="btn-ghost btn-sm btn"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setError('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorAlert;
|
||||||
@@ -3,11 +3,19 @@ import { FunctionComponent } from 'react';
|
|||||||
interface FormButtonProps {
|
interface FormButtonProps {
|
||||||
children: string;
|
children: string;
|
||||||
type: 'button' | 'submit' | 'reset';
|
type: 'button' | 'submit' | 'reset';
|
||||||
|
isSubmitting?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: FunctionComponent<FormButtonProps> = ({ children, type }) => (
|
const Button: FunctionComponent<FormButtonProps> = ({
|
||||||
|
children,
|
||||||
|
type,
|
||||||
|
isSubmitting = false,
|
||||||
|
}) => (
|
||||||
// eslint-disable-next-line react/button-has-type
|
// eslint-disable-next-line react/button-has-type
|
||||||
<button type={type} className="btn btn-primary mt-4 w-full rounded-xl">
|
<button
|
||||||
|
type={type}
|
||||||
|
className={`btn btn-primary mt-4 w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface FormSelectProps {
|
|||||||
error: boolean;
|
error: boolean;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,6 +41,7 @@ const FormSelect: FunctionComponent<FormSelectProps> = ({
|
|||||||
formRegister,
|
formRegister,
|
||||||
placeholder,
|
placeholder,
|
||||||
message,
|
message,
|
||||||
|
disabled = false,
|
||||||
}) => (
|
}) => (
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
@@ -47,6 +49,7 @@ const FormSelect: FunctionComponent<FormSelectProps> = ({
|
|||||||
error ? 'select-error' : ''
|
error ? 'select-error' : ''
|
||||||
}`}
|
}`}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
{...formRegister}
|
{...formRegister}
|
||||||
>
|
>
|
||||||
<option value="">{message}</option>
|
<option value="">{message}</option>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const withPageAuthRequired =
|
|||||||
}
|
}
|
||||||
return await fn(context);
|
return await fn(context);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: '/login',
|
destination: '/login',
|
||||||
|
|||||||
27
config/cloudinary/index.ts
Normal file
27
config/cloudinary/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import { v2 as cloudinary } from 'cloudinary';
|
||||||
|
import { CloudinaryStorage } from 'multer-storage-cloudinary';
|
||||||
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
|
const { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } = process.env;
|
||||||
|
|
||||||
|
if (!(CLOUDINARY_CLOUD_NAME && CLOUDINARY_KEY && CLOUDINARY_SECRET)) {
|
||||||
|
throw new ServerError(
|
||||||
|
'The cloudinary credentials were not found in the environment variables.',
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: CLOUDINARY_CLOUD_NAME,
|
||||||
|
api_key: CLOUDINARY_KEY,
|
||||||
|
api_secret: CLOUDINARY_SECRET,
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'BeerApp' } });
|
||||||
|
|
||||||
|
/** Configuration object for Cloudinary image upload. */
|
||||||
|
const cloudinaryConfig = { cloudinary, storage };
|
||||||
|
|
||||||
|
export default cloudinaryConfig;
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { Options } from 'next-connect';
|
import type { RequestHandler } from 'next-connect/dist/types/node';
|
||||||
|
import type { HandlerOptions } from 'next-connect/dist/types/types';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import logger from '../pino/logger';
|
||||||
|
|
||||||
import ServerError from '../util/ServerError';
|
import ServerError from '../util/ServerError';
|
||||||
|
|
||||||
const NextConnectConfig: Options<
|
type NextConnectOptionsT = HandlerOptions<
|
||||||
NextApiRequest,
|
RequestHandler<
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiRequest,
|
||||||
> = {
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const NextConnectOptions: NextConnectOptionsT = {
|
||||||
onNoMatch(req, res) {
|
onNoMatch(req, res) {
|
||||||
res.status(405).json({
|
res.status(405).json({
|
||||||
message: 'Method not allowed.',
|
message: 'Method not allowed.',
|
||||||
@@ -17,6 +23,10 @@ const NextConnectConfig: Options<
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError(error, req, res) {
|
onError(error, req, res) {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : 'Internal server error.';
|
const message = error instanceof Error ? error.message : 'Internal server error.';
|
||||||
const statusCode = error instanceof ServerError ? error.statusCode : 500;
|
const statusCode = error instanceof ServerError ? error.statusCode : 500;
|
||||||
res.status(statusCode).json({
|
res.status(statusCode).json({
|
||||||
@@ -27,4 +37,4 @@ const NextConnectConfig: Options<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NextConnectConfig;
|
export default NextConnectOptions;
|
||||||
31
config/nextConnect/middleware/checkIfBeerPostOwner.ts
Normal file
31
config/nextConnect/middleware/checkIfBeerPostOwner.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { NextHandler } from 'next-connect';
|
||||||
|
|
||||||
|
interface CheckIfBeerPostOwnerRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfBeerPostOwner = async <RequestType extends CheckIfBeerPostOwnerRequest>(
|
||||||
|
req: RequestType,
|
||||||
|
res: NextApiResponse,
|
||||||
|
next: NextHandler,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
const user = req.user!;
|
||||||
|
const beerPost = await getBeerPostById(id);
|
||||||
|
|
||||||
|
if (!beerPost) {
|
||||||
|
throw new ServerError('Beer post not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beerPost.postedBy.id !== user.id) {
|
||||||
|
throw new ServerError('You are not authorized to edit this beer post', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default checkIfBeerPostOwner;
|
||||||
@@ -2,8 +2,8 @@ import { NextApiResponse } from 'next';
|
|||||||
import { NextHandler } from 'next-connect';
|
import { NextHandler } from 'next-connect';
|
||||||
import findUserById from '@/services/User/findUserById';
|
import findUserById from '@/services/User/findUserById';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import { getLoginSession } from '../session';
|
import { getLoginSession } from '../../auth/session';
|
||||||
import { UserExtendedNextApiRequest } from '../types';
|
import { UserExtendedNextApiRequest } from '../../auth/types';
|
||||||
|
|
||||||
/** Get the current user from the session. Adds the user to the request object. */
|
/** Get the current user from the session. Adds the user to the request object. */
|
||||||
const getCurrentUser = async (
|
const getCurrentUser = async (
|
||||||
@@ -19,7 +19,7 @@ const getCurrentUser = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
await next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getCurrentUser;
|
export default getCurrentUser;
|
||||||
@@ -28,10 +28,11 @@ const validateRequest =
|
|||||||
}) =>
|
}) =>
|
||||||
async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => {
|
async (req: NextApiRequest, res: NextApiResponse, next: NextHandler) => {
|
||||||
if (bodySchema) {
|
if (bodySchema) {
|
||||||
const parsed = bodySchema.safeParse(req.body);
|
const parsed = bodySchema.safeParse(JSON.parse(JSON.stringify(req.body)));
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new ServerError('Invalid request body.', 400);
|
throw new ServerError('Invalid request body.', 400);
|
||||||
}
|
}
|
||||||
|
req.body = parsed.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (querySchema) {
|
if (querySchema) {
|
||||||
@@ -42,7 +43,7 @@ const validateRequest =
|
|||||||
req.query = parsed.data;
|
req.query = parsed.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default validateRequest;
|
export default validateRequest;
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino();
|
||||||
transport: {
|
|
||||||
target: 'pino-pretty',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default logger;
|
export default logger;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchem
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const useUser = () => {
|
const useUser = () => {
|
||||||
|
// check cookies for user
|
||||||
const {
|
const {
|
||||||
data: user,
|
data: user,
|
||||||
error,
|
error,
|
||||||
@@ -11,6 +12,7 @@ const useUser = () => {
|
|||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4264
package-lock.json
generated
4264
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
|||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"prestart": "npm run build",
|
"prestart": "npm run build",
|
||||||
"prismaDev": "dotenv -e .env.local prisma migrate dev",
|
"prismaDev": "dotenv -e .env.local prisma migrate dev",
|
||||||
|
"vercel-build": "DISABLE_ERD=true npx prisma generate && npx prisma migrate deploy && next build",
|
||||||
"seed": "npx ts-node ./prisma/seed/index.ts"
|
"seed": "npx ts-node ./prisma/seed/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -18,10 +19,13 @@
|
|||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^4.9.0",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
|
"cloudinary": "^1.33.0",
|
||||||
"cookie": "0.5.0",
|
"cookie": "0.5.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"multer-storage-cloudinary": "^4.0.0",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"next-connect": "^0.13.0",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pino": "^8.8.0",
|
"pino": "^8.8.0",
|
||||||
@@ -36,7 +40,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^7.6.0",
|
"@faker-js/faker": "^7.6.0",
|
||||||
|
"@mermaid-js/mermaid-cli": "^9.3.0",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/cookie": "^0.5.1",
|
||||||
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^18.13.0",
|
"@types/node": "^18.13.0",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
@@ -55,6 +61,7 @@
|
|||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||||
"prisma": "^4.9.0",
|
"prisma": "^4.9.0",
|
||||||
|
"prisma-erd-generator": "^1.2.5",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^4.9.5"
|
"typescript": "^4.9.5"
|
||||||
|
|||||||
16
pages/account/index.tsx
Normal file
16
pages/account/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Layout from '@/components/ui/Layout';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
|
interface AccountPageProps {}
|
||||||
|
|
||||||
|
const AccountPage: NextPage<AccountPageProps> = () => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div>
|
||||||
|
<h1>Account Page</h1>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountPage;
|
||||||
@@ -1,25 +1,28 @@
|
|||||||
import validateRequest from '@/config/zod/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
import createNewBeerComment from '@/services/BeerComment/createNewBeerComment';
|
||||||
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import { BeerCommentQueryResultT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
|
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import getCurrentUser from '@/config/auth/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
|
|
||||||
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
interface CreateCommentRequest extends UserExtendedNextApiRequest {
|
||||||
body: z.infer<typeof BeerCommentValidationSchema>;
|
body: z.infer<typeof BeerCommentValidationSchema>;
|
||||||
|
query: { id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
const createComment = async (
|
const createComment = async (
|
||||||
req: CreateCommentRequest,
|
req: CreateCommentRequest,
|
||||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
) => {
|
) => {
|
||||||
const { content, rating, beerPostId } = req.body;
|
const { content, rating } = req.body;
|
||||||
|
|
||||||
|
const beerPostId = req.query.id;
|
||||||
|
|
||||||
const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({
|
const newBeerComment: BeerCommentQueryResultT = await createNewBeerComment({
|
||||||
content,
|
content,
|
||||||
@@ -36,10 +39,19 @@ const createComment = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nextConnect(NextConnectConfig).post(
|
const router = createRouter<
|
||||||
validateRequest({ bodySchema: BeerCommentValidationSchema }),
|
CreateCommentRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
validateRequest({
|
||||||
|
bodySchema: BeerCommentValidationSchema,
|
||||||
|
querySchema: z.object({ id: z.string().uuid() }),
|
||||||
|
}),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
createComment,
|
createComment,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
98
pages/api/beers/[id]/images/index.ts
Normal file
98
pages/api/beers/[id]/images/index.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { BeerImage } from '@prisma/client';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import { createRouter, expressWrapper } from 'next-connect';
|
||||||
|
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
|
||||||
|
import multer from 'multer';
|
||||||
|
|
||||||
|
import cloudinaryConfig from '@/config/cloudinary';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
|
||||||
|
const { storage } = cloudinaryConfig;
|
||||||
|
|
||||||
|
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
|
||||||
|
const { mimetype } = file;
|
||||||
|
|
||||||
|
const isImage = mimetype.startsWith('image/');
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
cb(null, false);
|
||||||
|
}
|
||||||
|
cb(null, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadMiddleware = expressWrapper(
|
||||||
|
multer({ storage, fileFilter, limits: { files: 3 } }).array('images'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const BeerPostImageValidationSchema = z.object({
|
||||||
|
caption: z.string(),
|
||||||
|
alt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
|
||||||
|
files?: Express.Multer.File[];
|
||||||
|
query: { id: string };
|
||||||
|
body: z.infer<typeof BeerPostImageValidationSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processImageData = async (
|
||||||
|
req: UploadBeerPostImagesRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { files, user, body } = req;
|
||||||
|
|
||||||
|
if (!files || !files.length) {
|
||||||
|
throw new ServerError('No images uploaded', 400);
|
||||||
|
}
|
||||||
|
const beerImagePromises: Promise<BeerImage>[] = [];
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
beerImagePromises.push(
|
||||||
|
DBClient.instance.beerImage.create({
|
||||||
|
data: {
|
||||||
|
alt: body.alt,
|
||||||
|
postedBy: { connect: { id: user!.id } },
|
||||||
|
beerPost: { connect: { id: req.query.id } },
|
||||||
|
path: file.path,
|
||||||
|
caption: body.caption,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const beerImages = await Promise.all(beerImagePromises);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: `Successfully uploaded ${beerImages.length} image${
|
||||||
|
beerImages.length > 1 ? 's' : ''
|
||||||
|
}`,
|
||||||
|
statusCode: 200,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
UploadBeerPostImagesRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
getCurrentUser,
|
||||||
|
// @ts-expect-error
|
||||||
|
uploadMiddleware,
|
||||||
|
validateRequest({ bodySchema: BeerPostImageValidationSchema }),
|
||||||
|
processImageData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
|
|
||||||
|
export const config = { api: { bodyParser: false } };
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import validateRequest from '@/config/zod/middleware/validateRequest';
|
import { createRouter } from 'next-connect';
|
||||||
import getCurrentUser from '@/config/auth/middleware/getCurrentUser';
|
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
|
||||||
import nextConnect from 'next-connect';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
|
import createBeerPostLike from '@/services/BeerPostLike/createBeerPostLike';
|
||||||
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
|
import removeBeerPostLikeById from '@/services/BeerPostLike/removeBeerPostLikeById';
|
||||||
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
|
import findBeerPostLikeById from '@/services/BeerPostLike/findBeerPostLikeById';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
|
||||||
const sendLikeRequest = async (
|
const sendLikeRequest = async (
|
||||||
req: UserExtendedNextApiRequest,
|
req: UserExtendedNextApiRequest,
|
||||||
@@ -24,7 +24,7 @@ const sendLikeRequest = async (
|
|||||||
throw new ServerError('Could not find a beer post with that id', 404);
|
throw new ServerError('Could not find a beer post with that id', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const alreadyLiked = await findBeerPostLikeById(id);
|
const alreadyLiked = await findBeerPostLikeById(beer.id, user.id);
|
||||||
|
|
||||||
const jsonResponse = {
|
const jsonResponse = {
|
||||||
success: true as const,
|
success: true as const,
|
||||||
@@ -43,14 +43,16 @@ const sendLikeRequest = async (
|
|||||||
res.status(200).json(jsonResponse);
|
res.status(200).json(jsonResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nextConnect(NextConnectConfig).post(
|
const router = createRouter<
|
||||||
|
UserExtendedNextApiRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({
|
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
||||||
querySchema: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
sendLikeRequest,
|
sendLikeRequest,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import getCurrentUser from '@/config/auth/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import validateRequest from '@/config/zod/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const checkIfLiked = async (
|
const checkIfLiked = async (
|
||||||
@@ -18,7 +18,7 @@ const checkIfLiked = async (
|
|||||||
const alreadyLiked = await DBClient.instance.beerPostLike.findFirst({
|
const alreadyLiked = await DBClient.instance.beerPostLike.findFirst({
|
||||||
where: {
|
where: {
|
||||||
beerPostId: id,
|
beerPostId: id,
|
||||||
userId: user.id,
|
likedById: user.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,10 +30,20 @@ const checkIfLiked = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nextConnect(NextConnectConfig).get(
|
const router = createRouter<
|
||||||
|
UserExtendedNextApiRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.get(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
validateRequest({
|
||||||
|
querySchema: z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
checkIfLiked,
|
checkIfLiked,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import validateRequest from '@/config/zod/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import createNewBeerPost from '@/services/BeerPost/createNewBeerPost';
|
import createNewBeerPost from '@/services/BeerPost/createNewBeerPost';
|
||||||
import BeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import getCurrentUser from '@/config/auth/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
|
||||||
interface CreateBeerPostRequest extends UserExtendedNextApiRequest {
|
interface CreateBeerPostRequest extends UserExtendedNextApiRequest {
|
||||||
body: z.infer<typeof BeerPostValidationSchema>;
|
body: z.infer<typeof CreateBeerPostValidationSchema>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createBeerPost = async (
|
const createBeerPost = async (
|
||||||
@@ -37,10 +37,16 @@ const createBeerPost = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nextConnect(NextConnectConfig).post(
|
const router = createRouter<
|
||||||
validateRequest({ bodySchema: BeerPostValidationSchema }),
|
CreateBeerPostRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
validateRequest({ bodySchema: CreateBeerPostValidationSchema }),
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
createBeerPost,
|
createBeerPost,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import getCurrentUser from '@/config/auth/middleware/getCurrentUser';
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResponse) => {
|
const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResponse) => {
|
||||||
@@ -16,9 +16,12 @@ const sendCurrentUser = async (req: UserExtendedNextApiRequest, res: NextApiResp
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nextConnect<
|
const router = createRouter<
|
||||||
UserExtendedNextApiRequest,
|
UserExtendedNextApiRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
>(NextConnectConfig).get(getCurrentUser, sendCurrentUser);
|
>();
|
||||||
|
|
||||||
|
router.get(getCurrentUser, sendCurrentUser);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter, expressWrapper } from 'next-connect';
|
||||||
import localStrat from '@/config/auth/localStrat';
|
import localStrat from '@/config/auth/localStrat';
|
||||||
import { setLoginSession } from '@/config/auth/session';
|
import { setLoginSession } from '@/config/auth/session';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ServerError from '@/config/util/ServerError';
|
|
||||||
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
import LoginValidationSchema from '@/services/User/schema/LoginValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '../../../config/auth/types';
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
|
||||||
export default nextConnect<
|
const router = createRouter<
|
||||||
UserExtendedNextApiRequest,
|
UserExtendedNextApiRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
>(NextConnectConfig)
|
>();
|
||||||
.use(passport.initialize())
|
|
||||||
.use((req, res, next) => {
|
router.post(
|
||||||
const parsed = LoginValidationSchema.safeParse(req.body);
|
validateRequest({ bodySchema: LoginValidationSchema }),
|
||||||
if (!parsed.success) {
|
expressWrapper(async (req, res, next) => {
|
||||||
throw new ServerError('Username and password are required.', 400);
|
passport.initialize();
|
||||||
}
|
|
||||||
passport.use(localStrat);
|
passport.use(localStrat);
|
||||||
passport.authenticate('local', { session: false }, (error, token) => {
|
passport.authenticate('local', { session: false }, (error, token) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
next(error);
|
next(error);
|
||||||
} else {
|
return;
|
||||||
req.user = token;
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
|
req.user = token;
|
||||||
|
next();
|
||||||
})(req, res, next);
|
})(req, res, next);
|
||||||
})
|
}),
|
||||||
.post(async (req, res) => {
|
async (req, res) => {
|
||||||
const user = req.user!;
|
const user = req.user!;
|
||||||
await setLoginSession(res, user);
|
await setLoginSession(res, user);
|
||||||
|
|
||||||
@@ -40,4 +39,8 @@ export default nextConnect<
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { getLoginSession } from '@/config/auth/session';
|
import { getLoginSession } from '@/config/auth/session';
|
||||||
import { removeTokenCookie } from '@/config/auth/cookie';
|
import { removeTokenCookie } from '@/config/auth/cookie';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import nextConnect from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
|
||||||
const handler = nextConnect<
|
const router = createRouter<
|
||||||
NextApiRequest,
|
NextApiRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
>(NextConnectConfig).all(async (req, res) => {
|
>();
|
||||||
|
|
||||||
|
router.all(async (req, res) => {
|
||||||
const session = await getLoginSession(req);
|
const session = await getLoginSession(req);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
@@ -18,10 +20,9 @@ const handler = nextConnect<
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeTokenCookie(res);
|
removeTokenCookie(res);
|
||||||
res.status(200).json({
|
|
||||||
message: 'Logged out.',
|
res.redirect('/');
|
||||||
statusCode: 200,
|
|
||||||
success: true,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { setLoginSession } from '@/config/auth/session';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import nc from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
import createNewUser from '@/services/User/createNewUser';
|
import createNewUser from '@/services/User/createNewUser';
|
||||||
import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema';
|
import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema';
|
||||||
import NextConnectConfig from '@/config/nextConnect/NextConnectConfig';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import findUserByUsername from '@/services/User/findUserByUsername';
|
import findUserByUsername from '@/services/User/findUserByUsername';
|
||||||
import findUserByEmail from '@/services/User/findUserByEmail';
|
import findUserByEmail from '@/services/User/findUserByEmail';
|
||||||
import validateRequest from '@/config/zod/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
interface RegisterUserRequest extends NextApiRequest {
|
interface RegisterUserRequest extends NextApiRequest {
|
||||||
body: z.infer<typeof CreateUserValidationSchema>;
|
body: z.infer<typeof CreateUserValidationSchema>;
|
||||||
@@ -34,6 +36,11 @@ const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await createNewUser(req.body);
|
const user = await createNewUser(req.body);
|
||||||
|
|
||||||
|
await setLoginSession(res, {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
});
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
message: 'User created successfully.',
|
message: 'User created successfully.',
|
||||||
payload: user,
|
payload: user,
|
||||||
@@ -42,9 +49,12 @@ const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = nc(NextConnectConfig).post(
|
const router = createRouter<
|
||||||
validateRequest({ bodySchema: CreateUserValidationSchema }),
|
RegisterUserRequest,
|
||||||
registerUser,
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
);
|
>();
|
||||||
|
|
||||||
|
router.post(validateRequest({ bodySchema: CreateUserValidationSchema }), registerUser);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|||||||
@@ -4,41 +4,47 @@ import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
|||||||
import CommentCard from '@/components/BeerById/CommentCard';
|
import CommentCard from '@/components/BeerById/CommentCard';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
import getAllBeerComments from '@/services/BeerComment/getAllBeerComments';
|
||||||
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import { BeerCommentQueryResultArrayT } from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { BeerPost } from '@prisma/client';
|
import { BeerPost } from '@prisma/client';
|
||||||
import { NextPage, GetServerSideProps } from 'next';
|
import { NextPage, GetServerSideProps } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useState, useEffect, useContext } from 'react';
|
import { useState, useEffect, useContext } from 'react';
|
||||||
|
|
||||||
interface BeerPageProps {
|
interface BeerPageProps {
|
||||||
beerPost: BeerPostQueryResult;
|
beerPost: BeerPostQueryResult;
|
||||||
beerRecommendations: (BeerPost & {
|
beerRecommendations: (BeerPost & {
|
||||||
brewery: {
|
brewery: { id: string; name: string };
|
||||||
id: string;
|
beerImages: { id: string; alt: string; url: string }[];
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
beerImages: {
|
|
||||||
id: string;
|
|
||||||
alt: string;
|
|
||||||
url: string;
|
|
||||||
}[];
|
|
||||||
})[];
|
})[];
|
||||||
beerComments: BeerCommentQueryResultArrayT;
|
beerComments: BeerCommentQueryResultArrayT;
|
||||||
|
commentsPageCount: number;
|
||||||
|
likeCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeerByIdPage: NextPage<BeerPageProps> = ({
|
const BeerByIdPage: NextPage<BeerPageProps> = ({
|
||||||
beerPost,
|
beerPost,
|
||||||
beerRecommendations,
|
beerRecommendations,
|
||||||
beerComments,
|
beerComments,
|
||||||
|
commentsPageCount,
|
||||||
|
likeCount,
|
||||||
}) => {
|
}) => {
|
||||||
const { user } = useContext(UserContext);
|
const { user } = useContext(UserContext);
|
||||||
|
|
||||||
const [comments, setComments] = useState(beerComments);
|
const [comments, setComments] = useState(beerComments);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const commentsPageNum = router.query.comments_page
|
||||||
|
? parseInt(router.query.comments_page as string, 10)
|
||||||
|
: 1;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setComments(beerComments);
|
setComments(beerComments);
|
||||||
}, [beerComments]);
|
}, [beerComments]);
|
||||||
@@ -49,11 +55,11 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
<title>{beerPost.name}</title>
|
<title>{beerPost.name}</title>
|
||||||
<meta name="description" content={beerPost.description} />
|
<meta name="description" content={beerPost.description} />
|
||||||
</Head>
|
</Head>
|
||||||
<main>
|
<div>
|
||||||
{beerPost.beerImages[0] && (
|
{beerPost.beerImages[0] && (
|
||||||
<Image
|
<Image
|
||||||
alt={beerPost.beerImages[0].alt}
|
alt={beerPost.beerImages[0].alt}
|
||||||
src={beerPost.beerImages[0].url}
|
src={beerPost.beerImages[0].path}
|
||||||
height={1080}
|
height={1080}
|
||||||
width={1920}
|
width={1920}
|
||||||
className="h-[42rem] w-full object-cover"
|
className="h-[42rem] w-full object-cover"
|
||||||
@@ -61,10 +67,10 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="my-12 flex w-full items-center justify-center ">
|
<div className="my-12 flex w-full items-center justify-center ">
|
||||||
<div className="w-10/12 space-y-3">
|
<div className="w-11/12 space-y-3 lg:w-9/12">
|
||||||
<BeerInfoHeader beerPost={beerPost} />
|
<BeerInfoHeader beerPost={beerPost} initialLikeCount={likeCount} />
|
||||||
<div className="mt-4 flex space-x-3">
|
<div className="mt-4 flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-3">
|
||||||
<div className="w-[60%] space-y-3">
|
<div className="w-full space-y-3 sm:w-[60%]">
|
||||||
<div className="card h-96 bg-base-300">
|
<div className="card h-96 bg-base-300">
|
||||||
<div className="card-body h-full">
|
<div className="card-body h-full">
|
||||||
{user ? (
|
{user ? (
|
||||||
@@ -78,19 +84,52 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card h-[135rem] bg-base-300">
|
<div className="card bg-base-300 pb-6">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<CommentCard key={comment.id} comment={comment} />
|
<CommentCard key={comment.id} comment={comment} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="btn-group grid w-6/12 grid-cols-2">
|
||||||
|
<Link
|
||||||
|
className={`btn-outline btn ${
|
||||||
|
commentsPageNum === 1
|
||||||
|
? 'btn-disabled pointer-events-none'
|
||||||
|
: 'pointer-events-auto'
|
||||||
|
}`}
|
||||||
|
href={{
|
||||||
|
pathname: `/beers/${beerPost.id}`,
|
||||||
|
query: { comments_page: commentsPageNum - 1 },
|
||||||
|
}}
|
||||||
|
scroll={false}
|
||||||
|
>
|
||||||
|
Next Comments
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className={`btn-outline btn ${
|
||||||
|
commentsPageNum === commentsPageCount
|
||||||
|
? 'btn-disabled pointer-events-none'
|
||||||
|
: 'pointer-events-auto'
|
||||||
|
}`}
|
||||||
|
href={{
|
||||||
|
pathname: `/beers/${beerPost.id}`,
|
||||||
|
query: { comments_page: commentsPageNum + 1 },
|
||||||
|
}}
|
||||||
|
scroll={false}
|
||||||
|
>
|
||||||
|
Previous Comments
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[40%]">
|
<div className="sm:w-[40%]">
|
||||||
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
<BeerRecommendations beerRecommendations={beerRecommendations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -98,21 +137,34 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({
|
|||||||
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<BeerPageProps> = async (context) => {
|
||||||
const beerPost = await getBeerPostById(context.params!.id! as string);
|
const beerPost = await getBeerPostById(context.params!.id! as string);
|
||||||
|
|
||||||
|
const beerCommentPageNum = parseInt(context.query.comments_page as string, 10) || 1;
|
||||||
|
|
||||||
if (!beerPost) {
|
if (!beerPost) {
|
||||||
return { notFound: true };
|
return { notFound: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { type, brewery, id } = beerPost;
|
const { type, brewery, id } = beerPost;
|
||||||
|
const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
|
||||||
|
|
||||||
|
const pageSize = 5;
|
||||||
const beerComments = await getAllBeerComments(
|
const beerComments = await getAllBeerComments(
|
||||||
{ id: beerPost.id },
|
{ id: beerPost.id },
|
||||||
{ pageSize: 9, pageNum: 1 },
|
{ pageSize, pageNum: beerCommentPageNum },
|
||||||
);
|
);
|
||||||
const beerRecommendations = await getBeerRecommendations({ type, brewery, id });
|
const numberOfPosts = await DBClient.instance.beerComment.count({
|
||||||
|
where: { beerPostId: beerPost.id },
|
||||||
|
});
|
||||||
|
const pageCount = numberOfPosts ? Math.ceil(numberOfPosts / pageSize) : 0;
|
||||||
|
const likeCount = await DBClient.instance.beerPostLike.count({
|
||||||
|
where: { beerPostId: beerPost.id },
|
||||||
|
});
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
beerPost: JSON.parse(JSON.stringify(beerPost)),
|
||||||
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
beerRecommendations: JSON.parse(JSON.stringify(beerRecommendations)),
|
||||||
beerComments: JSON.parse(JSON.stringify(beerComments)),
|
beerComments: JSON.parse(JSON.stringify(beerComments)),
|
||||||
|
commentsPageCount: JSON.parse(JSON.stringify(pageCount)),
|
||||||
|
likeCount: JSON.parse(JSON.stringify(likeCount)),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { props };
|
return { props };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import BeerForm from '@/components/BeerForm';
|
import BeerForm from '@/components/BeerForm';
|
||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
|
import withPageAuthRequired from '@/config/auth/withPageAuthRequired';
|
||||||
|
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||||
@@ -30,7 +31,7 @@ const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = async () => {
|
export const getServerSideProps = withPageAuthRequired(async () => {
|
||||||
const breweryPosts = await getAllBreweryPosts();
|
const breweryPosts = await getAllBreweryPosts();
|
||||||
const beerTypes = await DBClient.instance.beerType.findMany();
|
const beerTypes = await DBClient.instance.beerType.findMany();
|
||||||
|
|
||||||
@@ -40,6 +41,6 @@ export const getServerSideProps = async () => {
|
|||||||
types: JSON.parse(JSON.stringify(beerTypes)),
|
types: JSON.parse(JSON.stringify(beerTypes)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Create;
|
export default Create;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import DBClient from '@/prisma/DBClient';
|
|||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import Pagination from '@/components/BeerIndex/Pagination';
|
import Pagination from '@/components/BeerIndex/Pagination';
|
||||||
import BeerCard from '@/components/BeerIndex/BeerCard';
|
import BeerCard from '@/components/BeerIndex/BeerCard';
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
interface BeerPageProps {
|
interface BeerPageProps {
|
||||||
initialBeerPosts: BeerPostQueryResult[];
|
initialBeerPosts: BeerPostQueryResult[];
|
||||||
@@ -20,6 +21,10 @@ const BeerPage: NextPage<BeerPageProps> = ({ initialBeerPosts, pageCount }) => {
|
|||||||
const pageNum = parseInt(query.page_num as string, 10) || 1;
|
const pageNum = parseInt(query.page_num as string, 10) || 1;
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<Head>
|
||||||
|
<title>Beer</title>
|
||||||
|
<meta name="description" content="Beer posts" />
|
||||||
|
</Head>
|
||||||
<div className="flex items-center justify-center bg-base-100">
|
<div className="flex items-center justify-center bg-base-100">
|
||||||
<main className="my-10 flex w-10/12 flex-col space-y-4">
|
<main className="my-10 flex w-10/12 flex-col space-y-4">
|
||||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { useRouter } from 'next/router';
|
|||||||
import Layout from '@/components/ui/Layout';
|
import Layout from '@/components/ui/Layout';
|
||||||
import useUser from '@/hooks/useUser';
|
import useUser from '@/hooks/useUser';
|
||||||
import LoginForm from '@/components/Login/LoginForm';
|
import LoginForm from '@/components/Login/LoginForm';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
import { FaUserCircle } from 'react-icons/fa';
|
||||||
|
import Head from 'next/head';
|
||||||
|
|
||||||
const LoginPage: NextPage = () => {
|
const LoginPage: NextPage = () => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
@@ -19,12 +23,31 @@ const LoginPage: NextPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<Head>
|
||||||
|
<title>Login</title>
|
||||||
|
<meta name="description" content="Login to your account" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
<div className="flex h-full flex-row">
|
<div className="flex h-full flex-row">
|
||||||
<div className="flex h-full w-[40%] flex-col items-center justify-center bg-base-100">
|
<div className="hidden h-full flex-col items-center justify-center bg-base-100 lg:flex lg:w-[60%]">
|
||||||
<h1>Login</h1>
|
<Image
|
||||||
|
src="https://picsum.photos/1040/1080"
|
||||||
|
alt="Login Image"
|
||||||
|
width={4920}
|
||||||
|
height={4080}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-[60%] flex-col items-center justify-center bg-base-300">
|
<div className="flex h-full w-full flex-col items-center space-y-5 bg-base-300 lg:w-[40%]">
|
||||||
<LoginForm />
|
<div className="mt-44 w-9/12">
|
||||||
|
<div className=" flex flex-col items-center space-y-2">
|
||||||
|
<FaUserCircle className="text-3xl" />
|
||||||
|
<h1 className="text-4xl font-bold">Login</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<LoginForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
168
pages/register/index.tsx
Normal file
168
pages/register/index.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import ErrorAlert from '@/components/ui/alerts/ErrorAlert';
|
||||||
|
import Button from '@/components/ui/forms/Button';
|
||||||
|
import FormError from '@/components/ui/forms/FormError';
|
||||||
|
import FormInfo from '@/components/ui/forms/FormInfo';
|
||||||
|
import FormLabel from '@/components/ui/forms/FormLabel';
|
||||||
|
import FormSegment from '@/components/ui/forms/FormSegment';
|
||||||
|
import FormTextInput from '@/components/ui/forms/FormTextInput';
|
||||||
|
import Layout from '@/components/ui/Layout';
|
||||||
|
import sendRegisterUserRequest from '@/requests/sendRegisterUserRequest';
|
||||||
|
import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { NextPage } from 'next';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { FaUserCircle } from 'react-icons/fa';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface RegisterUserProps {}
|
||||||
|
|
||||||
|
const RegisterUserPage: NextPage<RegisterUserProps> = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { reset, register, handleSubmit, formState } = useForm<
|
||||||
|
z.infer<typeof CreateUserValidationSchema>
|
||||||
|
>({
|
||||||
|
resolver: zodResolver(CreateUserValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
const [serverResponseError, setServerResponseError] = useState('');
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof CreateUserValidationSchema>) => {
|
||||||
|
try {
|
||||||
|
await sendRegisterUserRequest(data);
|
||||||
|
reset();
|
||||||
|
router.push('/', undefined, { shallow: true });
|
||||||
|
} catch (error) {
|
||||||
|
setServerResponseError(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Something went wrong. We could not register your account.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div
|
||||||
|
className="flex h-full flex-col items-center justify-center space-y-6"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<FaUserCircle className="text-3xl" />
|
||||||
|
<h1 className="text-4xl font-bold">Register</h1>
|
||||||
|
</div>
|
||||||
|
<form className="form-control w-7/12 space-y-5" noValidate>
|
||||||
|
{serverResponseError && (
|
||||||
|
<ErrorAlert error={serverResponseError} setError={setServerResponseError} />
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row space-x-3">
|
||||||
|
<div className="w-[50%]">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="firstName">First name</FormLabel>
|
||||||
|
<FormError>{errors.firstName?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="firstName"
|
||||||
|
type="text"
|
||||||
|
formValidationSchema={register('firstName')}
|
||||||
|
error={!!errors.firstName}
|
||||||
|
placeholder="first name"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-[50%]">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="lastName">Last name</FormLabel>
|
||||||
|
<FormError>{errors.lastName?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="lastName"
|
||||||
|
type="text"
|
||||||
|
formValidationSchema={register('lastName')}
|
||||||
|
error={!!errors.lastName}
|
||||||
|
placeholder="last name"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="username">username</FormLabel>
|
||||||
|
<FormError>{errors.username?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
formValidationSchema={register('username')}
|
||||||
|
error={!!errors.username}
|
||||||
|
placeholder="username"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="email">email</FormLabel>
|
||||||
|
<FormError>{errors.email?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
formValidationSchema={register('email')}
|
||||||
|
error={!!errors.email}
|
||||||
|
placeholder="email"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="password">password</FormLabel>
|
||||||
|
<FormError>{errors.password?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
formValidationSchema={register('password')}
|
||||||
|
error={!!errors.password}
|
||||||
|
placeholder="password"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="confirmPassword">confirm password</FormLabel>
|
||||||
|
<FormError>{errors.confirmPassword?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
formValidationSchema={register('confirmPassword')}
|
||||||
|
error={!!errors.confirmPassword}
|
||||||
|
placeholder="confirm password"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="dateOfBirth">Date of birth</FormLabel>
|
||||||
|
<FormError>{errors.dateOfBirth?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
id="dateOfBirth"
|
||||||
|
type="date"
|
||||||
|
formValidationSchema={register('dateOfBirth')}
|
||||||
|
error={!!errors.dateOfBirth}
|
||||||
|
placeholder="date of birth"
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<Button type="submit">Register User</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterUserPage;
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
||||||
|
|
||||||
const DBClient = {
|
const DBClient = {
|
||||||
instance: new PrismaClient(),
|
instance:
|
||||||
|
globalForPrisma.prisma ||
|
||||||
|
new PrismaClient({
|
||||||
|
log: ['info', 'warn'],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IDBClient = typeof DBClient;
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
globalForPrisma.prisma = DBClient.instance;
|
||||||
Object.freeze(DBClient);
|
}
|
||||||
|
|
||||||
export default DBClient;
|
export default DBClient;
|
||||||
|
|||||||
1
prisma/ERD.svg
Normal file
1
prisma/ERD.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 104 KiB |
30
prisma/migrations/20230211021836_/migration.sql
Normal file
30
prisma/migrations/20230211021836_/migration.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `url` on the `BeerImage` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `url` on the `BreweryImage` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `caption` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `path` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `postedById` to the `BeerImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `caption` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `path` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
- Added the required column `postedById` to the `BreweryImage` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BeerImage" DROP COLUMN "url",
|
||||||
|
ADD COLUMN "caption" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "path" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "postedById" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BreweryImage" DROP COLUMN "url",
|
||||||
|
ADD COLUMN "caption" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "path" TEXT NOT NULL,
|
||||||
|
ADD COLUMN "postedById" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerImage" ADD CONSTRAINT "BeerImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BreweryImage" ADD CONSTRAINT "BreweryImage_postedById_fkey" FOREIGN KEY ("postedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
16
prisma/migrations/20230213214022_/migration.sql
Normal file
16
prisma/migrations/20230213214022_/migration.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `userId` on the `BeerPostLike` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `likedById` to the `BeerPostLike` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "BeerPostLike" DROP CONSTRAINT "BeerPostLike_userId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BeerPostLike" DROP COLUMN "userId",
|
||||||
|
ADD COLUMN "likedById" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "BeerPostLike" ADD CONSTRAINT "BeerPostLike_likedById_fkey" FOREIGN KEY ("likedById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -5,6 +5,13 @@ generator client {
|
|||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator erdSVG {
|
||||||
|
provider = "prisma-erd-generator"
|
||||||
|
output = "./ERD.svg"
|
||||||
|
includeRelationFromFields = true
|
||||||
|
theme = "neutral"
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
@@ -26,6 +33,8 @@ model User {
|
|||||||
beerComments BeerComment[]
|
beerComments BeerComment[]
|
||||||
breweryComments BreweryComment[]
|
breweryComments BreweryComment[]
|
||||||
BeerPostLikes BeerPostLike[]
|
BeerPostLikes BeerPostLike[]
|
||||||
|
BeerImage BeerImage[]
|
||||||
|
BreweryImage BreweryImage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model BeerPost {
|
model BeerPost {
|
||||||
@@ -51,8 +60,8 @@ model BeerPostLike {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
||||||
beerPostId String
|
beerPostId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
likedBy User @relation(fields: [likedById], references: [id], onDelete: Cascade)
|
||||||
userId String
|
likedById String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
}
|
}
|
||||||
@@ -109,18 +118,24 @@ model BeerImage {
|
|||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
beerPost BeerPost @relation(fields: [beerPostId], references: [id], onDelete: Cascade)
|
||||||
beerPostId String
|
beerPostId String
|
||||||
url String
|
path String
|
||||||
alt String
|
alt String
|
||||||
|
caption String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
|
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||||
|
postedById String
|
||||||
}
|
}
|
||||||
|
|
||||||
model BreweryImage {
|
model BreweryImage {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
breweryPost BreweryPost @relation(fields: [breweryPostId], references: [id], onDelete: Cascade)
|
||||||
breweryPostId String
|
breweryPostId String
|
||||||
url String
|
path String
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||||
|
caption String
|
||||||
alt String
|
alt String
|
||||||
|
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||||
|
postedById String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ const cleanDatabase = async () => {
|
|||||||
await prisma.$executeRaw`TRUNCATE TABLE "BreweryPost" CASCADE`;
|
await prisma.$executeRaw`TRUNCATE TABLE "BreweryPost" CASCADE`;
|
||||||
await prisma.$executeRaw`TRUNCATE TABLE "BeerComment" CASCADE`;
|
await prisma.$executeRaw`TRUNCATE TABLE "BeerComment" CASCADE`;
|
||||||
await prisma.$executeRaw`TRUNCATE TABLE "BreweryComment" CASCADE`;
|
await prisma.$executeRaw`TRUNCATE TABLE "BreweryComment" CASCADE`;
|
||||||
|
await prisma.$executeRaw`TRUNCATE TABLE "BeerPostLike" CASCADE`;
|
||||||
|
await prisma.$executeRaw`TRUNCATE TABLE "BeerImage" CASCADE`;
|
||||||
|
await prisma.$executeRaw`TRUNCATE TABLE "BreweryImage" CASCADE`;
|
||||||
|
|
||||||
|
await prisma.$disconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default cleanDatabase;
|
export default cleanDatabase;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BeerPost, BeerImage } from '@prisma/client';
|
import { BeerPost, BeerImage, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateNewBeerImagesArgs {
|
interface CreateNewBeerImagesArgs {
|
||||||
numberOfImages: number;
|
numberOfImages: number;
|
||||||
beerPosts: BeerPost[];
|
joinData: { beerPosts: BeerPost[]; users: User[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewBeerImages = async ({
|
const createNewBeerImages = async ({
|
||||||
numberOfImages,
|
numberOfImages,
|
||||||
beerPosts,
|
joinData: { beerPosts, users },
|
||||||
}: CreateNewBeerImagesArgs) => {
|
}: CreateNewBeerImagesArgs) => {
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
@@ -18,12 +19,15 @@ const createNewBeerImages = async ({
|
|||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfImages; i++) {
|
for (let i = 0; i < numberOfImages; i++) {
|
||||||
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
beerImagesPromises.push(
|
beerImagesPromises.push(
|
||||||
prisma.beerImage.create({
|
prisma.beerImage.create({
|
||||||
data: {
|
data: {
|
||||||
url: 'https://picsum.photos/900/1600',
|
path: 'https://picsum.photos/1040/1080',
|
||||||
alt: 'Placeholder beer image.',
|
alt: 'Placeholder beer image.',
|
||||||
|
caption: 'Placeholder beer image caption.',
|
||||||
beerPost: { connect: { id: beerPost.id } },
|
beerPost: { connect: { id: beerPost.id } },
|
||||||
|
postedBy: { connect: { id: user.id } },
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const createNewBeerPostLikes = async ({
|
|||||||
DBClient.instance.beerPostLike.create({
|
DBClient.instance.beerPostLike.create({
|
||||||
data: {
|
data: {
|
||||||
beerPost: { connect: { id: beerPost.id } },
|
beerPost: { connect: { id: beerPost.id } },
|
||||||
user: { connect: { id: user.id } },
|
likedBy: { connect: { id: user.id } },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { BreweryPost, BreweryImage } from '@prisma/client';
|
import { BreweryPost, BreweryImage, User } from '@prisma/client';
|
||||||
import DBClient from '../../DBClient';
|
import DBClient from '../../DBClient';
|
||||||
|
|
||||||
interface CreateBreweryImagesArgs {
|
interface CreateBreweryImagesArgs {
|
||||||
numberOfImages: number;
|
numberOfImages: number;
|
||||||
breweryPosts: BreweryPost[];
|
|
||||||
|
joinData: {
|
||||||
|
breweryPosts: BreweryPost[];
|
||||||
|
users: User[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const createNewBreweryImages = async ({
|
const createNewBreweryImages = async ({
|
||||||
numberOfImages,
|
numberOfImages,
|
||||||
breweryPosts,
|
joinData: { breweryPosts, users },
|
||||||
}: CreateBreweryImagesArgs) => {
|
}: CreateBreweryImagesArgs) => {
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
const createdAt = faker.date.past(1);
|
const createdAt = faker.date.past(1);
|
||||||
@@ -18,13 +22,16 @@ const createNewBreweryImages = async ({
|
|||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfImages; i++) {
|
for (let i = 0; i < numberOfImages; i++) {
|
||||||
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||||
|
const user = users[Math.floor(Math.random() * users.length)];
|
||||||
|
|
||||||
breweryImagesPromises.push(
|
breweryImagesPromises.push(
|
||||||
prisma.breweryImage.create({
|
prisma.breweryImage.create({
|
||||||
data: {
|
data: {
|
||||||
url: 'https://picsum.photos/900/1600',
|
path: 'https://picsum.photos/1040/1080',
|
||||||
alt: 'Placeholder brewery image.',
|
alt: 'Placeholder brewery image.',
|
||||||
|
caption: 'Placeholder brewery image caption.',
|
||||||
breweryPost: { connect: { id: breweryPost.id } },
|
breweryPost: { connect: { id: breweryPost.id } },
|
||||||
|
postedBy: { connect: { id: user.id } },
|
||||||
createdAt,
|
createdAt,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
|||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < numberOfUsers; i++) {
|
for (let i = 0; i < numberOfUsers; i++) {
|
||||||
const randomValue = crypto.randomBytes(2).toString('hex');
|
const randomValue = crypto.randomBytes(4).toString('hex');
|
||||||
const firstName = faker.name.firstName();
|
const firstName = faker.name.firstName();
|
||||||
const lastName = faker.name.lastName();
|
const lastName = faker.name.lastName();
|
||||||
const username = `${firstName[0]}.${lastName}.${randomValue}`;
|
const username = `${firstName[0]}.${lastName}.${randomValue}`;
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ import createNewUsers from './create/createNewUsers';
|
|||||||
}),
|
}),
|
||||||
createNewBeerImages({
|
createNewBeerImages({
|
||||||
numberOfImages: 1000,
|
numberOfImages: 1000,
|
||||||
beerPosts,
|
joinData: { beerPosts, users },
|
||||||
}),
|
}),
|
||||||
createNewBreweryImages({
|
createNewBreweryImages({
|
||||||
numberOfImages: 1000,
|
numberOfImages: 1000,
|
||||||
breweryPosts,
|
joinData: { breweryPosts, users },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBee
|
|||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const BeerCommentValidationSchemaWithId = BeerCommentValidationSchema.extend({
|
||||||
|
beerPostId: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
const sendCreateBeerCommentRequest = async ({
|
const sendCreateBeerCommentRequest = async ({
|
||||||
beerPostId,
|
beerPostId,
|
||||||
content,
|
content,
|
||||||
rating,
|
rating,
|
||||||
}: z.infer<typeof BeerCommentValidationSchema>) => {
|
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
|
||||||
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
|
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -29,7 +33,6 @@ const sendCreateBeerCommentRequest = async ({
|
|||||||
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
const parsedResponse = APIResponseValidationSchema.safeParse(data);
|
||||||
|
|
||||||
if (!parsedResponse.success) {
|
if (!parsedResponse.success) {
|
||||||
console.log(parsedResponse.error);
|
|
||||||
throw new Error('Invalid API response');
|
throw new Error('Invalid API response');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +40,6 @@ const sendCreateBeerCommentRequest = async ({
|
|||||||
const parsedPayload = BeerCommentQueryResult.safeParse(parsedResponse.data.payload);
|
const parsedPayload = BeerCommentQueryResult.safeParse(parsedResponse.data.payload);
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
console.log(parsedPayload.error);
|
|
||||||
throw new Error('Invalid API response payload');
|
throw new Error('Invalid API response payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,36 @@
|
|||||||
import BeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
import { beerPostQueryResultSchema } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
import CreateBeerPostValidationSchema from '@/services/BeerPost/schema/CreateBeerPostValidationSchema';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const sendCreateBeerPostRequest = async (
|
const sendCreateBeerPostRequest = async (
|
||||||
data: z.infer<typeof BeerPostValidationSchema>,
|
data: z.infer<typeof CreateBeerPostValidationSchema>,
|
||||||
) => {
|
) => {
|
||||||
const response = await fetch('/api/beers/create', {
|
const response = await fetch('/api/beers/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new Error('Invalid API response');
|
throw new Error('Invalid API response');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { payload } = parsed.data;
|
const { payload, success, message } = parsed.data;
|
||||||
|
|
||||||
if (
|
if (!success) {
|
||||||
!(
|
throw new Error(message);
|
||||||
payload &&
|
|
||||||
typeof payload === 'object' &&
|
|
||||||
'id' in payload &&
|
|
||||||
typeof payload.id === 'string'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error('Invalid API response');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload;
|
const parsedPayload = beerPostQueryResultSchema.safeParse(payload);
|
||||||
|
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('Invalid API response payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default sendCreateBeerPostRequest;
|
export default sendCreateBeerPostRequest;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
const sendLikeRequest = async (beerPost: BeerPostQueryResult) => {
|
const sendLikeRequest = async (beerPostId: string) => {
|
||||||
const response = await fetch(`/api/beers/${beerPost.id}/like`, {
|
const response = await fetch(`/api/beers/${beerPostId}/like`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ const sendLoginUserRequest = async (data: { username: string; password: string }
|
|||||||
throw new Error('API response validation failed');
|
throw new Error('API response validation failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!parsed.data.success) {
|
||||||
|
throw new Error(parsed.data.message);
|
||||||
|
}
|
||||||
const parsedPayload = BasicUserInfoSchema.safeParse(parsed.data.payload);
|
const parsedPayload = BasicUserInfoSchema.safeParse(parsed.data.payload);
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error('API response payload validation failed');
|
throw new Error('API response payload validation failed');
|
||||||
|
|||||||
35
requests/sendRegisterUserRequest.ts
Normal file
35
requests/sendRegisterUserRequest.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import CreateUserValidationSchema from '@/services/User/schema/CreateUserValidationSchema';
|
||||||
|
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
async function sendRegisterUserRequest(data: z.infer<typeof CreateUserValidationSchema>) {
|
||||||
|
const response = await fetch('/api/users/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error('API response validation failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.data.success) {
|
||||||
|
throw new Error(parsed.data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
||||||
|
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('API response payload validation failed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sendRegisterUserRequest;
|
||||||
@@ -2,8 +2,9 @@ import DBClient from '@/prisma/DBClient';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema';
|
import BeerCommentValidationSchema from './schema/CreateBeerCommentValidationSchema';
|
||||||
|
|
||||||
const CreateBeerCommentWithUserSchema = BeerCommentValidationSchema.extend({
|
const CreateNewBeerCommentServiceSchema = BeerCommentValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
|
beerPostId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createNewBeerComment = async ({
|
const createNewBeerComment = async ({
|
||||||
@@ -11,7 +12,7 @@ const createNewBeerComment = async ({
|
|||||||
rating,
|
rating,
|
||||||
beerPostId,
|
beerPostId,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateBeerCommentWithUserSchema>) => {
|
}: z.infer<typeof CreateNewBeerCommentServiceSchema>) => {
|
||||||
return DBClient.instance.beerComment.create({
|
return DBClient.instance.beerComment.create({
|
||||||
data: {
|
data: {
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BeerPostQueryResult from '../BeerPost/schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { BeerCommentQueryResultArrayT } from './schema/BeerCommentQueryResult';
|
import { BeerCommentQueryResultArrayT } from './schema/BeerCommentQueryResult';
|
||||||
|
|
||||||
const getAllBeerComments = async (
|
const getAllBeerComments = async (
|
||||||
@@ -9,27 +9,17 @@ const getAllBeerComments = async (
|
|||||||
const skip = (pageNum - 1) * pageSize;
|
const skip = (pageNum - 1) * pageSize;
|
||||||
const beerComments: BeerCommentQueryResultArrayT =
|
const beerComments: BeerCommentQueryResultArrayT =
|
||||||
await DBClient.instance.beerComment.findMany({
|
await DBClient.instance.beerComment.findMany({
|
||||||
where: {
|
skip,
|
||||||
beerPostId: id,
|
take: pageSize,
|
||||||
},
|
where: { beerPostId: id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
content: true,
|
content: true,
|
||||||
rating: true,
|
rating: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
postedBy: {
|
postedBy: { select: { id: true, username: true, createdAt: true } },
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: { createdAt: 'desc' },
|
||||||
createdAt: 'desc',
|
|
||||||
},
|
|
||||||
skip,
|
|
||||||
take: pageSize,
|
|
||||||
});
|
});
|
||||||
return beerComments;
|
return beerComments;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const BeerCommentQueryResult = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
content: z.string().min(1).max(300),
|
content: z.string().min(1).max(300),
|
||||||
rating: z.number().int().min(1).max(5),
|
rating: z.number().int().min(1).max(5),
|
||||||
createdAt: z.date().or(z.string().datetime()),
|
createdAt: z.coerce.date(),
|
||||||
postedBy: z.object({
|
postedBy: z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
username: z.string().min(1).max(50),
|
username: z.string().min(1).max(50),
|
||||||
|
|||||||
@@ -3,20 +3,13 @@ import { z } from 'zod';
|
|||||||
const BeerCommentValidationSchema = z.object({
|
const BeerCommentValidationSchema = z.object({
|
||||||
content: z
|
content: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, { message: 'Comment must not be empty.' })
|
||||||
message: 'Comment must not be empty.',
|
.max(300, { message: 'Comment must be less than 300 characters.' }),
|
||||||
})
|
|
||||||
.max(300, {
|
|
||||||
message: 'Comment must be less than 300 characters.',
|
|
||||||
}),
|
|
||||||
rating: z
|
rating: z
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(1, { message: 'Rating must be greater than 1.' })
|
.min(1, { message: 'Rating must be greater than 1.' })
|
||||||
.max(5, { message: 'Rating must be less than 5.' }),
|
.max(5, { message: 'Rating must be less than 5.' }),
|
||||||
beerPostId: z.string().uuid({
|
|
||||||
message: 'Beer post ID must be a valid UUID.',
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BeerCommentValidationSchema;
|
export default BeerCommentValidationSchema;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import BeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
import { BeerPostQueryResult } from './schema/BeerPostQueryResult';
|
||||||
|
import CreateBeerPostValidationSchema from './schema/CreateBeerPostValidationSchema';
|
||||||
|
|
||||||
const CreateBeerPostWithUserSchema = BeerPostValidationSchema.extend({
|
const CreateBeerPostWithUserSchema = CreateBeerPostValidationSchema.extend({
|
||||||
userId: z.string().uuid(),
|
userId: z.string().uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ const createNewBeerPost = async ({
|
|||||||
breweryId,
|
breweryId,
|
||||||
userId,
|
userId,
|
||||||
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
}: z.infer<typeof CreateBeerPostWithUserSchema>) => {
|
||||||
const newBeerPost = await DBClient.instance.beerPost.create({
|
const newBeerPost: BeerPostQueryResult = await DBClient.instance.beerPost.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@@ -25,6 +26,18 @@ const createNewBeerPost = async ({
|
|||||||
postedBy: { connect: { id: userId } },
|
postedBy: { connect: { id: userId } },
|
||||||
brewery: { connect: { id: breweryId } },
|
brewery: { connect: { id: breweryId } },
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
description: true,
|
||||||
|
abv: true,
|
||||||
|
ibu: true,
|
||||||
|
createdAt: true,
|
||||||
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
|
brewery: { select: { id: true, name: true } },
|
||||||
|
type: { select: { id: true, name: true } },
|
||||||
|
postedBy: { select: { id: true, username: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return newBeerPost;
|
return newBeerPost;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
@@ -10,35 +10,14 @@ const getAllBeerPosts = async (pageNum: number, pageSize: number) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
type: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ibu: true,
|
ibu: true,
|
||||||
abv: true,
|
abv: true,
|
||||||
brewery: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: true,
|
description: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
postedBy: {
|
type: { select: { name: true, id: true } },
|
||||||
select: {
|
brewery: { select: { name: true, id: true } },
|
||||||
id: true,
|
postedBy: { select: { id: true, username: true } },
|
||||||
username: true,
|
beerImages: { select: { path: true, caption: true, id: true, alt: true } },
|
||||||
},
|
|
||||||
},
|
|
||||||
beerImages: {
|
|
||||||
select: {
|
|
||||||
url: true,
|
|
||||||
id: true,
|
|
||||||
alt: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
skip,
|
skip,
|
||||||
|
|||||||
@@ -1,60 +1,23 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const prisma = DBClient.instance;
|
const prisma = DBClient.instance;
|
||||||
|
|
||||||
const getBeerPostById = async (id: string) => {
|
const getBeerPostById = async (id: string) => {
|
||||||
const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({
|
const beerPost: BeerPostQueryResult | null = await prisma.beerPost.findFirst({
|
||||||
select: {
|
select: {
|
||||||
beerComments: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
content: true,
|
|
||||||
createdAt: true,
|
|
||||||
postedBy: {
|
|
||||||
select: {
|
|
||||||
username: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rating: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
brewery: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ibu: true,
|
ibu: true,
|
||||||
abv: true,
|
abv: true,
|
||||||
type: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beerImages: {
|
|
||||||
select: {
|
|
||||||
alt: true,
|
|
||||||
url: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
description: true,
|
description: true,
|
||||||
postedBy: {
|
postedBy: { select: { username: true, id: true } },
|
||||||
select: {
|
brewery: { select: { name: true, id: true } },
|
||||||
username: true,
|
type: { select: { name: true, id: true } },
|
||||||
id: true,
|
beerImages: { select: { alt: true, path: true, caption: true, id: true } },
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
},
|
||||||
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
return beerPost;
|
return beerPost;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import BeerPostQueryResult from './schema/BeerPostQueryResult';
|
import { BeerPostQueryResult } from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
|
|
||||||
const getBeerRecommendations = async (
|
const getBeerRecommendations = async (
|
||||||
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery' | 'id'>,
|
beerPost: Pick<BeerPostQueryResult, 'type' | 'brewery' | 'id'>,
|
||||||
@@ -10,7 +10,7 @@ const getBeerRecommendations = async (
|
|||||||
NOT: { id: beerPost.id },
|
NOT: { id: beerPost.id },
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
beerImages: { select: { id: true, url: true, alt: true } },
|
beerImages: { select: { id: true, path: true, caption: true, alt: true } },
|
||||||
brewery: { select: { id: true, name: true } },
|
brewery: { select: { id: true, name: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,27 +1,36 @@
|
|||||||
export default interface BeerPostQueryResult {
|
import { z } from 'zod';
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
brewery: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
description: string;
|
|
||||||
beerImages: {
|
|
||||||
url: string;
|
|
||||||
id: string;
|
|
||||||
alt: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
ibu: number;
|
export const beerPostQueryResultSchema = z.object({
|
||||||
abv: number;
|
id: z.string(),
|
||||||
type: {
|
name: z.string(),
|
||||||
id: string;
|
brewery: z.object({
|
||||||
name: string;
|
id: z.string(),
|
||||||
};
|
name: z.string(),
|
||||||
postedBy: {
|
}),
|
||||||
id: string;
|
description: z.string(),
|
||||||
username: string;
|
beerImages: z.array(
|
||||||
};
|
z.object({
|
||||||
|
path: z.string(),
|
||||||
|
caption: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
alt: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
ibu: z.number(),
|
||||||
|
abv: z.number(),
|
||||||
|
type: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
postedBy: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
}),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
});
|
||||||
|
|
||||||
createdAt: Date;
|
export const beerPostQueryResultArraySchema = z.array(beerPostQueryResultSchema);
|
||||||
}
|
|
||||||
|
export type BeerPostQueryResult = z.infer<typeof beerPostQueryResultSchema>;
|
||||||
|
|
||||||
|
export type BeerPostQueryResultArray = z.infer<typeof beerPostQueryResultArraySchema>;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const BeerPostValidationSchema = z.object({
|
const CreateBeerPostValidationSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string({
|
.string({
|
||||||
required_error: 'Beer name is required.',
|
required_error: 'Beer name is required.',
|
||||||
@@ -40,4 +40,4 @@ const BeerPostValidationSchema = z.object({
|
|||||||
.uuid({ message: 'Invalid brewery id.' }),
|
.uuid({ message: 'Invalid brewery id.' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BeerPostValidationSchema;
|
export default CreateBeerPostValidationSchema;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const createBeerPostLike = async ({
|
|||||||
return DBClient.instance.beerPostLike.create({
|
return DBClient.instance.beerPostLike.create({
|
||||||
data: {
|
data: {
|
||||||
beerPost: { connect: { id } },
|
beerPost: { connect: { id } },
|
||||||
user: { connect: { id: user.id } },
|
likedBy: { connect: { id: user.id } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
const findBeerPostLikeById = async (id: string) =>
|
const findBeerPostLikeById = async (beerPostId: string, likedById: string) =>
|
||||||
DBClient.instance.beerPostLike.findUnique({ where: { id } });
|
DBClient.instance.beerPostLike.findFirst({ where: { beerPostId, likedById } });
|
||||||
|
|
||||||
export default findBeerPostLikeById;
|
export default findBeerPostLikeById;
|
||||||
|
|||||||
@@ -2,36 +2,52 @@ import sub from 'date-fns/sub';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
const minimumDateOfBirth = sub(new Date(), { years: 19 });
|
const minimumDateOfBirth = sub(new Date(), { years: 19 });
|
||||||
const CreateUserValidationSchema = z.object({
|
const CreateUserValidationSchema = z
|
||||||
email: z.string().email({ message: 'Email must be a valid email address.' }),
|
.object({
|
||||||
// use special characters, numbers, and uppercase letters
|
email: z.string().email({ message: 'Email must be a valid email address.' }),
|
||||||
password: z
|
// use special characters, numbers, and uppercase letters
|
||||||
.string()
|
password: z
|
||||||
.min(8, { message: 'Password must be at least 8 characters.' })
|
.string()
|
||||||
.refine((password) => /[A-Z]/.test(password), {
|
.min(8, { message: 'Password must be at least 8 characters.' })
|
||||||
message: 'Password must contain at least one uppercase letter.',
|
.refine((password) => /[A-Z]/.test(password), {
|
||||||
})
|
message: 'Password must contain at least one uppercase letter.',
|
||||||
.refine((password) => /[0-9]/.test(password), {
|
})
|
||||||
message: 'Password must contain at least one number.',
|
.refine((password) => /[0-9]/.test(password), {
|
||||||
})
|
message: 'Password must contain at least one number.',
|
||||||
.refine((password) => /[^a-zA-Z0-9]/.test(password), {
|
})
|
||||||
message: 'Password must contain at least one special character.',
|
.refine((password) => /[^a-zA-Z0-9]/.test(password), {
|
||||||
}),
|
message: 'Password must contain at least one special character.',
|
||||||
|
}),
|
||||||
firstName: z.string().min(1, { message: 'First name must not be empty.' }),
|
confirmPassword: z.string(),
|
||||||
lastName: z.string().min(1, { message: 'Last name must not be empty.' }),
|
firstName: z
|
||||||
username: z
|
.string()
|
||||||
.string()
|
.min(1, { message: 'First name must not be empty.' })
|
||||||
.min(1, { message: 'Username must not be empty.' })
|
.max(20, { message: 'First name must be less than 20 characters.' })
|
||||||
.max(20, { message: 'Username must be less than 20 characters.' }),
|
.refine((firstName) => /^[a-zA-Z]+$/.test(firstName), {
|
||||||
dateOfBirth: z.string().refine(
|
message: 'First name must only contain letters.',
|
||||||
(dateOfBirth) => {
|
}),
|
||||||
const parsedDateOfBirth = new Date(dateOfBirth);
|
lastName: z
|
||||||
|
.string()
|
||||||
return parsedDateOfBirth <= minimumDateOfBirth;
|
.min(1, { message: 'Last name must not be empty.' })
|
||||||
},
|
.max(20, { message: 'Last name must be less than 20 characters.' })
|
||||||
{ message: 'You must be at least 19 years old to register.' },
|
.refine((lastName) => /^[a-zA-Z]+$/.test(lastName), {
|
||||||
),
|
message: 'Last name must only contain letters.',
|
||||||
});
|
}),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Username must not be empty.' })
|
||||||
|
.max(20, { message: 'Username must be less than 20 characters.' }),
|
||||||
|
dateOfBirth: z.string().refine(
|
||||||
|
(dateOfBirth) => {
|
||||||
|
const parsedDateOfBirth = new Date(dateOfBirth);
|
||||||
|
return parsedDateOfBirth <= minimumDateOfBirth;
|
||||||
|
},
|
||||||
|
{ message: 'You must be at least 19 years old to register.' },
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match.',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
export default CreateUserValidationSchema;
|
export default CreateUserValidationSchema;
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { z } from 'zod';
|
|||||||
const GetUserSchema = z.object({
|
const GetUserSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
createdAt: z.date().or(z.string()),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.date().or(z.string()).optional(),
|
updatedAt: z.coerce.date().optional(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
dateOfBirth: z.date().or(z.string()),
|
dateOfBirth: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default GetUserSchema;
|
export default GetUserSchema;
|
||||||
|
|||||||
Reference in New Issue
Block a user