mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 20:13:49 +00:00
Merge pull request #29 from aaronpo97/dev
Feature: Edit beer comments, dark/light mode switch
This commit is contained in:
23
package-lock.json
generated
23
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"react-responsive-carousel": "^3.2.23",
|
"react-responsive-carousel": "^3.2.23",
|
||||||
"sparkpost": "^2.1.4",
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.1.2",
|
"swr": "^2.1.2",
|
||||||
|
"theme-change": "^2.5.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -9643,6 +9644,11 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/theme-change": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -10213,9 +10219,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vm2": {
|
"node_modules/vm2": {
|
||||||
"version": "3.9.16",
|
"version": "3.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
|
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz",
|
||||||
"integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
|
"integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.7.0",
|
"acorn": "^8.7.0",
|
||||||
@@ -17148,6 +17154,11 @@
|
|||||||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"theme-change": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/theme-change/-/theme-change-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ=="
|
||||||
|
},
|
||||||
"thenify": {
|
"thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -17582,9 +17593,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vm2": {
|
"vm2": {
|
||||||
"version": "3.9.16",
|
"version": "3.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.16.tgz",
|
"resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.17.tgz",
|
||||||
"integrity": "sha512-3T9LscojNTxdOyG+e8gFeyBXkMlOBYDoF6dqZbj+MPVHi9x10UfiTAJIobuchRCp3QvC+inybTbMJIUrLsig0w==",
|
"integrity": "sha512-AqwtCnZ/ERcX+AVj9vUsphY56YANXxRuqMb7GsDtAr0m0PcQX3u0Aj3KWiXM0YAHy7i6JEeHrwOnwXbGYgRpAw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"acorn": "^8.7.0",
|
"acorn": "^8.7.0",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"react-responsive-carousel": "^3.2.23",
|
"react-responsive-carousel": "^3.2.23",
|
||||||
"sparkpost": "^2.1.4",
|
"sparkpost": "^2.1.4",
|
||||||
"swr": "^2.1.2",
|
"swr": "^2.1.2",
|
||||||
|
"theme-change": "^2.5.0",
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1 +1,11 @@
|
|||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
|
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
|
|||||||
beerRecommendations,
|
beerRecommendations,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="card sticky top-2 h-full overflow-y-scroll bg-base-300">
|
<div className="card sticky top-2 h-full overflow-y-scroll">
|
||||||
<div className="card-body space-y-3">
|
<div className="card-body space-y-3">
|
||||||
{beerRecommendations.map((beerPost) => (
|
{beerRecommendations.map((beerPost) => (
|
||||||
<div key={beerPost.id} className="w-full">
|
<div key={beerPost.id} className="w-full">
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import UserContext from '@/contexts/userContext';
|
|
||||||
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
import useTimeDistance from '@/hooks/useTimeDistance';
|
|
||||||
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
import format from 'date-fns/format';
|
import { FC, useState } from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { FC, useContext } from 'react';
|
|
||||||
import { Rating } from 'react-daisyui';
|
|
||||||
|
|
||||||
import { FaEllipsisH } from 'react-icons/fa';
|
|
||||||
import { useInView } from 'react-intersection-observer';
|
import { useInView } from 'react-intersection-observer';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import CommentContentBody from './CommentContentBody';
|
||||||
|
import EditCommentBody from './EditCommentBody';
|
||||||
|
|
||||||
interface CommentCardProps {
|
interface CommentCardProps {
|
||||||
comment: z.infer<typeof BeerCommentQueryResult>;
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
@@ -17,93 +12,18 @@ interface CommentCardProps {
|
|||||||
ref?: ReturnType<typeof useInView>['ref'];
|
ref?: ReturnType<typeof useInView>['ref'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CommentCardDropdown: FC<CommentCardProps> = ({ comment, mutate }) => {
|
|
||||||
const { user } = useContext(UserContext);
|
|
||||||
|
|
||||||
const isCommentOwner = user?.id === comment.postedBy.id;
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to delete comment');
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
<label tabIndex={0} className="btn-ghost btn-sm btn m-1">
|
|
||||||
<FaEllipsisH />
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="dropdown-content menu rounded-box w-52 bg-base-100 p-2 shadow"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
{isCommentOwner ? (
|
|
||||||
<>
|
|
||||||
<button type="button">Edit</button>
|
|
||||||
<button type="button" onClick={handleDelete}>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<button>Report</button>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CommentCardBody: FC<CommentCardProps> = ({ comment, mutate, ref }) => {
|
const CommentCardBody: FC<CommentCardProps> = ({ comment, mutate, ref }) => {
|
||||||
const { user } = useContext(UserContext);
|
const [inEditMode, setInEditMode] = useState(false);
|
||||||
|
|
||||||
const timeDistance = useTimeDistance(new Date(comment.createdAt));
|
return !inEditMode ? (
|
||||||
|
<CommentContentBody comment={comment} ref={ref} setInEditMode={setInEditMode} />
|
||||||
return (
|
) : (
|
||||||
<div className="card-body animate-in fade-in-10" ref={ref}>
|
<EditCommentBody
|
||||||
<div className="flex flex-row justify-between">
|
comment={comment}
|
||||||
<div>
|
mutate={mutate}
|
||||||
<h3 className="font-semibold sm:text-2xl">
|
setInEditMode={setInEditMode}
|
||||||
<Link href={`/users/${comment.postedBy.id}`} className="link-hover link">
|
ref={ref}
|
||||||
{comment.postedBy.username}
|
/>
|
||||||
</Link>
|
|
||||||
</h3>
|
|
||||||
<h4 className="italic">
|
|
||||||
posted{' '}
|
|
||||||
<time
|
|
||||||
className="tooltip tooltip-bottom"
|
|
||||||
data-tip={format(new Date(comment.createdAt), 'MM/dd/yyyy')}
|
|
||||||
>
|
|
||||||
{timeDistance}
|
|
||||||
</time>{' '}
|
|
||||||
ago
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{user && <CommentCardDropdown comment={comment} mutate={mutate} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Rating value={comment.rating}>
|
|
||||||
{Array.from({ length: 5 }).map((val, index) => (
|
|
||||||
<Rating.Item
|
|
||||||
name="rating-1"
|
|
||||||
className="mask mask-star cursor-default"
|
|
||||||
disabled
|
|
||||||
aria-disabled
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Rating>
|
|
||||||
<p>{comment.content}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
47
src/components/BeerById/CommentCardDropdown.tsx
Normal file
47
src/components/BeerById/CommentCardDropdown.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import { Dispatch, SetStateAction, FC, useContext } from 'react';
|
||||||
|
import { FaEllipsisH } from 'react-icons/fa';
|
||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface CommentCardDropdownProps {
|
||||||
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentCardDropdown: FC<CommentCardDropdownProps> = ({
|
||||||
|
comment,
|
||||||
|
setInEditMode,
|
||||||
|
}) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const isCommentOwner = user?.id === comment.postedBy.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dropdown-end dropdown">
|
||||||
|
<label tabIndex={0} className="btn-ghost btn-sm btn m-1">
|
||||||
|
<FaEllipsisH />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="dropdown-content menu rounded-box w-52 bg-base-100 p-2 shadow"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
{isCommentOwner ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setInEditMode(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button>Report</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentCardDropdown;
|
||||||
67
src/components/BeerById/CommentContentBody.tsx
Normal file
67
src/components/BeerById/CommentContentBody.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import UserContext from '@/contexts/userContext';
|
||||||
|
import useTimeDistance from '@/hooks/useTimeDistance';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { Dispatch, FC, SetStateAction, useContext } from 'react';
|
||||||
|
import { Link, Rating } from 'react-daisyui';
|
||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import CommentCardDropdown from './CommentCardDropdown';
|
||||||
|
|
||||||
|
interface CommentContentBodyProps {
|
||||||
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
|
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
||||||
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CommentContentBody: FC<CommentContentBodyProps> = ({
|
||||||
|
comment,
|
||||||
|
ref,
|
||||||
|
setInEditMode,
|
||||||
|
}) => {
|
||||||
|
const { user } = useContext(UserContext);
|
||||||
|
const timeDistance = useTimeDistance(new Date(comment.createdAt));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card-body animate-in fade-in-10" ref={ref}>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold sm:text-2xl">
|
||||||
|
<Link href={`/users/${comment.postedBy.id}`} className="link-hover link">
|
||||||
|
{comment.postedBy.username}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
<h4 className="italic">
|
||||||
|
posted{' '}
|
||||||
|
<time
|
||||||
|
className="tooltip tooltip-bottom"
|
||||||
|
data-tip={format(new Date(comment.createdAt), 'MM/dd/yyyy')}
|
||||||
|
>
|
||||||
|
{timeDistance}
|
||||||
|
</time>{' '}
|
||||||
|
ago
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user && <CommentCardDropdown comment={comment} setInEditMode={setInEditMode} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Rating value={comment.rating}>
|
||||||
|
{Array.from({ length: 5 }).map((val, index) => (
|
||||||
|
<Rating.Item
|
||||||
|
name="rating-1"
|
||||||
|
className="mask mask-star cursor-default"
|
||||||
|
disabled
|
||||||
|
aria-disabled
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Rating>
|
||||||
|
<p>{comment.content}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentContentBody;
|
||||||
158
src/components/BeerById/EditCommentBody.tsx
Normal file
158
src/components/BeerById/EditCommentBody.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { FC, useState, useEffect, Dispatch, SetStateAction } from 'react';
|
||||||
|
import { Rating } from 'react-daisyui';
|
||||||
|
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useBeerPostComments from '@/hooks/useBeerPostComments';
|
||||||
|
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import FormError from '../ui/forms/FormError';
|
||||||
|
import FormInfo from '../ui/forms/FormInfo';
|
||||||
|
import FormLabel from '../ui/forms/FormLabel';
|
||||||
|
import FormSegment from '../ui/forms/FormSegment';
|
||||||
|
import FormTextArea from '../ui/forms/FormTextArea';
|
||||||
|
|
||||||
|
interface CommentCardDropdownProps {
|
||||||
|
comment: z.infer<typeof BeerCommentQueryResult>;
|
||||||
|
setInEditMode: Dispatch<SetStateAction<boolean>>;
|
||||||
|
ref: ReturnType<typeof useInView>['ref'] | undefined;
|
||||||
|
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditCommentBody: FC<CommentCardDropdownProps> = ({
|
||||||
|
comment,
|
||||||
|
setInEditMode,
|
||||||
|
ref,
|
||||||
|
mutate,
|
||||||
|
}) => {
|
||||||
|
const { register, handleSubmit, formState, setValue, watch } = useForm<
|
||||||
|
z.infer<typeof BeerCommentValidationSchema>
|
||||||
|
>({
|
||||||
|
defaultValues: {
|
||||||
|
content: comment.content,
|
||||||
|
rating: comment.rating,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(BeerCommentValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { errors } = formState;
|
||||||
|
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsDeleting(false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to delete comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
|
||||||
|
data,
|
||||||
|
) => {
|
||||||
|
const response = await fetch(`/api/beer-comments/${comment.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: data.content,
|
||||||
|
rating: data.rating,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to update comment');
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutate();
|
||||||
|
setInEditMode(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="card-body animate-in fade-in-10" ref={ref}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="content">Edit your comment</FormLabel>
|
||||||
|
<FormError>{errors.content?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextArea
|
||||||
|
id="content"
|
||||||
|
formValidationSchema={register('content')}
|
||||||
|
placeholder="Comment"
|
||||||
|
rows={2}
|
||||||
|
error={!!errors.content?.message}
|
||||||
|
disabled={formState.isSubmitting || isDeleting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="rating">Change your rating</FormLabel>
|
||||||
|
<FormError>{errors.rating?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<Rating
|
||||||
|
value={watch('rating')}
|
||||||
|
onChange={(value) => {
|
||||||
|
setValue('rating', value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 5 }).map((val, index) => (
|
||||||
|
<Rating.Item
|
||||||
|
name="rating-1"
|
||||||
|
className="mask mask-star cursor-default"
|
||||||
|
disabled={formState.isSubmitting || isDeleting}
|
||||||
|
aria-disabled={formState.isSubmitting || isDeleting}
|
||||||
|
key={index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Rating>
|
||||||
|
</div>
|
||||||
|
<div className="btn-group btn-group-horizontal">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-xs btn lg:btn-sm"
|
||||||
|
disabled={formState.isSubmitting || isDeleting}
|
||||||
|
onClick={() => {
|
||||||
|
setInEditMode(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={formState.isSubmitting || isDeleting}
|
||||||
|
className="btn-xs btn lg:btn-sm"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-xs btn lg:btn-sm"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isDeleting || formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCommentBody;
|
||||||
@@ -2,8 +2,9 @@ import useMediaQuery from '@/hooks/useMediaQuery';
|
|||||||
import useNavbar from '@/hooks/useNavbar';
|
import useNavbar from '@/hooks/useNavbar';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
|
import { MdDarkMode, MdLightMode } from 'react-icons/md';
|
||||||
import { GiHamburgerMenu } from 'react-icons/gi';
|
import { GiHamburgerMenu } from 'react-icons/gi';
|
||||||
|
import useTheme from '@/hooks/useTheme';
|
||||||
|
|
||||||
const DesktopLinks: FC = () => {
|
const DesktopLinks: FC = () => {
|
||||||
const { pages, currentURL } = useNavbar();
|
const { pages, currentURL } = useNavbar();
|
||||||
@@ -17,7 +18,7 @@ const DesktopLinks: FC = () => {
|
|||||||
<Link tabIndex={0} href={page.slug}>
|
<Link tabIndex={0} href={page.slug}>
|
||||||
<span
|
<span
|
||||||
className={`text-lg uppercase ${
|
className={`text-lg uppercase ${
|
||||||
currentURL === page.slug ? 'font-extrabold' : 'font-semibold'
|
currentURL === page.slug ? 'font-black' : 'font-medium'
|
||||||
} text-primary-content`}
|
} text-primary-content`}
|
||||||
>
|
>
|
||||||
{page.name}
|
{page.name}
|
||||||
@@ -59,6 +60,8 @@ const MobileLinks: FC = () => {
|
|||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const isDesktopView = useMediaQuery('(min-width: 1024px)');
|
const isDesktopView = useMediaQuery('(min-width: 1024px)');
|
||||||
|
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="navbar sticky top-0 z-50 bg-primary text-primary-content">
|
<nav className="navbar sticky top-0 z-50 bg-primary text-primary-content">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -66,6 +69,33 @@ const Navbar = () => {
|
|||||||
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
|
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="tooltip tooltip-left"
|
||||||
|
data-tip={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{theme === 'light' ? (
|
||||||
|
<button
|
||||||
|
className="btn-ghost btn-md btn-circle btn"
|
||||||
|
data-set-theme="dark"
|
||||||
|
data-act-class="ACTIVECLASS"
|
||||||
|
onClick={() => setTheme('dark')}
|
||||||
|
>
|
||||||
|
<MdLightMode className="text-xl" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn-ghost btn-md btn-circle btn"
|
||||||
|
data-set-theme="light"
|
||||||
|
data-act-class="ACTIVECLASS"
|
||||||
|
onClick={() => setTheme('light')}
|
||||||
|
>
|
||||||
|
<MdDarkMode className="text-xl" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
|
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ interface ErrorAlertProps {
|
|||||||
|
|
||||||
const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
|
const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
|
||||||
return (
|
return (
|
||||||
<div className="alert alert-error shadow-lg">
|
<div className="alert alert-error flex-row shadow-lg">
|
||||||
<div>
|
<div className="space-x-1">
|
||||||
<FiAlertTriangle className="h-6 w-6" />
|
<FiAlertTriangle className="h-6 w-6" />
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface FormLabelProps {
|
|||||||
*/
|
*/
|
||||||
const FormLabel: FunctionComponent<FormLabelProps> = ({ htmlFor, children }) => (
|
const FormLabel: FunctionComponent<FormLabelProps> = ({ htmlFor, children }) => (
|
||||||
<label
|
<label
|
||||||
className="my-1 block text-sm font-extrabold uppercase tracking-wide sm:text-xs"
|
className="my-1 block text-xs font-extrabold uppercase tracking-wide lg:text-sm"
|
||||||
htmlFor={htmlFor}
|
htmlFor={htmlFor}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
|
|||||||
backLinkText,
|
backLinkText,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="align-center my-20 flex flex-col items-center justify-center">
|
<div className="my-20 flex flex-col items-center justify-center">
|
||||||
<div className="w-10/12 lg:w-8/12 2xl:w-6/12">
|
<div className="w-10/12 lg:w-8/12 2xl:w-6/12">
|
||||||
<div className="tooltip tooltip-right" data-tip={backLinkText}>
|
<div className="tooltip tooltip-right" data-tip={backLinkText}>
|
||||||
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
||||||
@@ -28,7 +28,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center space-y-1">
|
<div className="flex flex-col items-center space-y-1">
|
||||||
{headingIcon({ className: 'text-4xl' })}{' '}
|
{headingIcon({ className: 'text-4xl' })}{' '}
|
||||||
<h1 className="text-3xl font-bold">{headingText}</h1>
|
<h1 className="text-center text-3xl font-bold">{headingText}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3">{FormComponent}</div>
|
<div className="mt-3">{FormComponent}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const FormTextArea: FunctionComponent<FormTextAreaProps> = ({
|
|||||||
<textarea
|
<textarea
|
||||||
id={id}
|
id={id}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={`textarea-bordered textarea m-0 w-full resize-none rounded-lg border border-solid bg-clip-padding transition ease-in-out ${
|
className={`text-md textarea-bordered textarea m-0 w-full resize-none rounded-lg border border-solid transition ease-in-out ${
|
||||||
error ? 'textarea-error' : ''
|
error ? 'textarea-error' : ''
|
||||||
}`}
|
}`}
|
||||||
{...formValidationSchema}
|
{...formValidationSchema}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const FormTextInput: FunctionComponent<FormInputProps> = ({
|
|||||||
id={id}
|
id={id}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={`input-bordered input w-full rounded-lg transition ease-in-out ${
|
className={`input-bordered input w-full appearance-none rounded-lg transition ease-in-out ${
|
||||||
error ? 'input-error' : ''
|
error ? 'input-error' : ''
|
||||||
}`}
|
}`}
|
||||||
{...formValidationSchema}
|
{...formValidationSchema}
|
||||||
|
|||||||
31
src/hooks/useTheme.ts
Normal file
31
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import useMediaQuery from './useMediaQuery';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook to manage the theme of the app. If a preferred theme is not set in
|
||||||
|
* localStorage, it will use what the user's browser prefers as determined by the
|
||||||
|
* prefers-color-scheme media query. If the user changes their preferred theme, it will be
|
||||||
|
* saved in localStorage and used in subsequent visits.
|
||||||
|
*
|
||||||
|
* @returns ThemeState.theme - The current theme of the app.
|
||||||
|
* @returns ThemeState.setTheme - A setter function to change the theme of the app.
|
||||||
|
*/
|
||||||
|
const useTheme = () => {
|
||||||
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||||
|
|
||||||
|
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (prefersDarkMode && !savedTheme) {
|
||||||
|
setTheme('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTheme(savedTheme as 'light' | 'dark');
|
||||||
|
}, [prefersDarkMode, theme]);
|
||||||
|
|
||||||
|
return { theme, setTheme };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTheme;
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
// create a 404 next js page using tailwind
|
// create a 404 next js page using tailwind
|
||||||
|
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
|
||||||
const NotFound: NextPage = () => {
|
const NotFound: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>404 Page Not Found</title>
|
<title>404 Page Not Found</title>
|
||||||
<meta name="description" content="404 Page Not Found" />
|
<meta name="description" content="404 Page Not Found" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
|
||||||
<h1 className="text-7xl font-bold">Error: 404</h1>
|
<h1 className="text-3xl font-bold lg:text-5xl">404: Not Found</h1>
|
||||||
<h2 className="text-xl font-bold">Page Not Found</h2>
|
<h2 className="text-lg font-bold">
|
||||||
|
Sorry, the page you are looking for does not exist.
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
|
||||||
const ServerErrorPage: NextPage = () => {
|
const ServerErrorPage: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>500 Internal Server Error</title>
|
<title>500 Internal Server Error</title>
|
||||||
<meta name="description" content="500 Internal Server Error" />
|
<meta name="description" content="500 Internal Server Error" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-4">
|
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
|
||||||
<h1 className="text-7xl font-bold">Error: 500</h1>
|
<h1 className="text-2xl font-bold lg:text-4xl">500: Something Went Wrong</h1>
|
||||||
<h2 className="text-xl font-bold">Internal Server Error</h2>
|
<h2 className="text-lg font-bold">
|
||||||
|
Please try again later or contact us if the problem persists.
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ import UserContext from '@/contexts/userContext';
|
|||||||
import useUser from '@/hooks/useUser';
|
import useUser from '@/hooks/useUser';
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { themeChange } from 'theme-change';
|
||||||
|
|
||||||
import { Space_Grotesk } from 'next/font/google';
|
import { Space_Grotesk } from 'next/font/google';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Layout from '@/components/ui/Layout';
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
themeChange(false);
|
||||||
|
}, []);
|
||||||
const { user, isLoading, error, mutate } = useUser();
|
const { user, isLoading, error, mutate } = useUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -21,9 +28,16 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
|
<Head>
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
<UserContext.Provider value={{ user, isLoading, error, mutate }}>
|
<UserContext.Provider value={{ user, isLoading, error, mutate }}>
|
||||||
<Component {...pageProps} />
|
<Layout>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
interface AccountPageProps {}
|
interface AccountPageProps {}
|
||||||
|
|
||||||
const AccountPage: NextPage<AccountPageProps> = () => {
|
const AccountPage: NextPage<AccountPageProps> = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div>
|
||||||
<div>
|
<h1>Account Page</h1>
|
||||||
<h1>Account Page</h1>
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
107
src/pages/api/beer-comments/[id].ts
Normal file
107
src/pages/api/beer-comments/[id].ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { createRouter, NextHandler } from 'next-connect';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface DeleteCommentRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditCommentRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
body: z.infer<typeof BeerCommentValidationSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfCommentOwner = async (
|
||||||
|
req: DeleteCommentRequest | EditCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
next: NextHandler,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
const user = req.user!;
|
||||||
|
const comment = await DBClient.instance.beerComment.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
throw new ServerError('Comment not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.postedById !== user.id) {
|
||||||
|
throw new ServerError('You are not authorized to modify this comment', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const editComment = async (
|
||||||
|
req: EditCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
const updated = await DBClient.instance.beerComment.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
content: req.body.content,
|
||||||
|
rating: req.body.rating,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Comment updated successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
payload: updated,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteComment = async (
|
||||||
|
req: DeleteCommentRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
await DBClient.instance.beerComment.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Comment deleted successfully',
|
||||||
|
statusCode: 200,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
DeleteCommentRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router
|
||||||
|
.delete(
|
||||||
|
validateRequest({ querySchema: z.object({ id: z.string().uuid() }) }),
|
||||||
|
getCurrentUser,
|
||||||
|
checkIfCommentOwner,
|
||||||
|
deleteComment,
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({ id: z.string().uuid() }),
|
||||||
|
bodySchema: BeerCommentValidationSchema,
|
||||||
|
}),
|
||||||
|
getCurrentUser,
|
||||||
|
checkIfCommentOwner,
|
||||||
|
editComment,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
|
||||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
|
||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
|
||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
|
||||||
import ServerError from '@/config/util/ServerError';
|
|
||||||
import DBClient from '@/prisma/DBClient';
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
|
||||||
import { NextApiResponse } from 'next';
|
|
||||||
import { createRouter } from 'next-connect';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
interface DeleteCommentRequest extends UserExtendedNextApiRequest {
|
|
||||||
query: { id: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteComment = async (
|
|
||||||
req: DeleteCommentRequest,
|
|
||||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
|
||||||
) => {
|
|
||||||
const { id } = req.query;
|
|
||||||
const user = req.user!;
|
|
||||||
|
|
||||||
const comment = await DBClient.instance.beerComment.findUnique({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!comment) {
|
|
||||||
throw new ServerError('Comment not found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comment.postedById !== user.id) {
|
|
||||||
throw new ServerError('You are not authorized to delete this comment', 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
await DBClient.instance.beerComment.delete({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Comment deleted successfully',
|
|
||||||
statusCode: 200,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const router = createRouter<
|
|
||||||
DeleteCommentRequest,
|
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
|
||||||
>();
|
|
||||||
|
|
||||||
router.delete(
|
|
||||||
validateRequest({
|
|
||||||
querySchema: z.object({ id: z.string().uuid() }),
|
|
||||||
}),
|
|
||||||
getCurrentUser,
|
|
||||||
deleteComment,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handler = router.handler(NextConnectOptions);
|
|
||||||
export default handler;
|
|
||||||
@@ -2,7 +2,6 @@ import { NextPage } from 'next';
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
@@ -16,10 +15,10 @@ interface EditPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
||||||
const pageTitle = `Edit "${beerPost.name}"`;
|
const pageTitle = `Edit \u201c${beerPost.name}\u201d`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
<meta name="description" content={pageTitle} />
|
<meta name="description" content={pageTitle} />
|
||||||
@@ -41,7 +40,7 @@ const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormPageLayout>
|
</FormPageLayout>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import Image from 'next/image';
|
|||||||
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
|
import BeerInfoHeader from '@/components/BeerById/BeerInfoHeader';
|
||||||
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
|
import BeerPostCommentsSection from '@/components/BeerById/BeerPostCommentsSection';
|
||||||
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
import BeerRecommendations from '@/components/BeerById/BeerRecommendations';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
|
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
import getBeerRecommendations from '@/services/BeerPost/getBeerRecommendations';
|
||||||
@@ -37,7 +36,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
<title>{beerPost.name}</title>
|
<title>{beerPost.name}</title>
|
||||||
<meta name="description" content={beerPost.description} />
|
<meta name="description" content={beerPost.description} />
|
||||||
</Head>
|
</Head>
|
||||||
<Layout>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Carousel
|
<Carousel
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -55,12 +54,12 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
src={image.path}
|
src={image.path}
|
||||||
height={1080}
|
height={1080}
|
||||||
width={1920}
|
width={1920}
|
||||||
className="h-[42rem] w-full object-cover"
|
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
: Array.from({ length: 1 }).map((_, i) => (
|
: Array.from({ length: 1 }).map((_, i) => (
|
||||||
<div className="h-[42rem] bg-base-300" key={i} />
|
<div className="h-96 lg:h-[42rem]" key={i} />
|
||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
|
||||||
@@ -79,7 +78,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tab.Group>
|
<Tab.Group>
|
||||||
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl bg-base-300">
|
<Tab.List className="tabs tabs-boxed items-center justify-center rounded-2xl">
|
||||||
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
|
||||||
Comments
|
Comments
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -100,7 +99,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import CreateBeerPostForm from '@/components/CreateBeerPostForm';
|
import CreateBeerPostForm from '@/components/CreateBeerPostForm';
|
||||||
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||||
@@ -17,16 +17,14 @@ interface CreateBeerPageProps {
|
|||||||
|
|
||||||
const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
|
const Create: NextPage<CreateBeerPageProps> = ({ breweries, types }) => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<FormPageLayout
|
||||||
<FormPageLayout
|
headingText="Create a new beer"
|
||||||
headingText="Create a new beer"
|
headingIcon={BiBeer}
|
||||||
headingIcon={BiBeer}
|
backLink="/beers"
|
||||||
backLink="/beers"
|
backLinkText="Back to beers"
|
||||||
backLinkText="Back to beers"
|
>
|
||||||
>
|
<CreateBeerPostForm breweries={breweries} types={types} />
|
||||||
<CreateBeerPostForm breweries={breweries} types={types} />
|
</FormPageLayout>
|
||||||
</FormPageLayout>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import BeerCard from '@/components/BeerIndex/BeerCard';
|
import BeerCard from '@/components/BeerIndex/BeerCard';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -32,15 +32,15 @@ const BeerPage: NextPage = () => {
|
|||||||
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
const pageRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Beer</title>
|
<title>Beer</title>
|
||||||
<meta name="description" content="Beer posts" />
|
<meta name="description" content="Beer posts" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
|
<div className="flex items-center justify-center bg-base-100" ref={pageRef}>
|
||||||
<div className="my-10 flex w-11/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
|
<div className="my-10 flex w-10/12 flex-col space-y-4 lg:w-8/12 2xl:w-7/12">
|
||||||
<header className="my-10 flex justify-between lg:flex-row">
|
<header className="my-10 flex justify-between lg:flex-row">
|
||||||
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
|
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
|
||||||
{!!user && (
|
{!!user && (
|
||||||
<div
|
<div
|
||||||
className="tooltip tooltip-left h-full"
|
className="tooltip tooltip-left h-full"
|
||||||
@@ -105,7 +105,7 @@ const BeerPage: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
@@ -43,36 +42,34 @@ const SearchPage: NextPage = () => {
|
|||||||
const showSearchResults = !isLoading && searchResults && !searchError;
|
const showSearchResults = !isLoading && searchResults && !searchError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
<div className="h-full w-full space-y-20">
|
||||||
<div className="h-full w-full space-y-20">
|
<div className="flex h-[50%] w-full items-center justify-center bg-base-200">
|
||||||
<div className="flex h-[50%] w-full items-center justify-center bg-base-200">
|
<div className="w-8/12">
|
||||||
<div className="w-8/12">
|
<FormLabel htmlFor="search">What are you looking for?</FormLabel>
|
||||||
<FormLabel htmlFor="search">What are you looking for?</FormLabel>
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
id="search"
|
||||||
id="search"
|
className="input-bordered input w-full rounded-lg"
|
||||||
className="input-bordered input w-full rounded-lg"
|
onChange={onChange}
|
||||||
onChange={onChange}
|
value={searchValue}
|
||||||
value={searchValue}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
{!showSearchResults ? (
|
|
||||||
<Spinner size="lg" />
|
|
||||||
) : (
|
|
||||||
<div className="grid w-8/12 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{searchResults.map((result) => {
|
|
||||||
return <BeerCard key={result.id} post={result} />;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
{!showSearchResults ? (
|
||||||
|
<Spinner size="lg" />
|
||||||
|
) : (
|
||||||
|
<div className="grid w-8/12 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{searchResults.map((result) => {
|
||||||
|
return <BeerCard key={result.id} post={result} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
|
|
||||||
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
import getBreweryPostById from '@/services/BreweryPost/getBreweryPostById';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import { GetServerSideProps, NextPage } from 'next';
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
@@ -11,9 +9,9 @@ interface BreweryPageProps {
|
|||||||
|
|
||||||
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost }) => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<h1 className="text-3xl font-bold underline">{breweryPost.name}</h1>
|
<h1 className="text-3xl font-bold underline">{breweryPost.name}</h1>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { GetServerSideProps, NextPage } from 'next';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
import getAllBreweryPosts from '@/services/BreweryPost/getAllBreweryPosts';
|
||||||
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -16,7 +16,7 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
|||||||
brewery,
|
brewery,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-300" key={brewery.id}>
|
<div className="card" key={brewery.id}>
|
||||||
<figure className="card-image h-96">
|
<figure className="card-image h-96">
|
||||||
{brewery.breweryImages.length > 0 && (
|
{brewery.breweryImages.length > 0 && (
|
||||||
<Image
|
<Image
|
||||||
@@ -41,7 +41,7 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
|||||||
|
|
||||||
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<div className="flex items-center justify-center bg-base-100">
|
<div className="flex items-center justify-center bg-base-100">
|
||||||
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
<div className="my-10 flex w-10/12 flex-col space-y-4">
|
||||||
<header className="my-10">
|
<header className="my-10">
|
||||||
@@ -56,7 +56,7 @@ const BreweryPage: NextPage<BreweryPageProps> = ({ breweryPosts }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
|
||||||
@@ -7,21 +6,23 @@ const Home: NextPage = () => {
|
|||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>The Biergarten App</title>
|
<title>The Biergarten App</title>
|
||||||
<meta name="description" content="Home" />
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="The Biergarten App is an app for beer lovers to share their favourite brews and breweries with like-minded people online."
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<Layout>
|
|
||||||
<div className="flex h-full w-full items-center justify-center bg-primary">
|
<div className="flex h-full w-full items-center justify-center bg-primary">
|
||||||
<div className="w-9/12 text-center lg:w-8/12">
|
<div className="w-9/12 text-center lg:w-8/12">
|
||||||
<h1 className="text-3xl font-bold md:text-4xl lg:text-5xl xl:text-8xl">
|
<h1 className="text-3xl font-bold md:text-4xl lg:text-5xl xl:text-8xl">
|
||||||
The Biergarten App
|
The Biergarten App
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4 text-lg lg:text-2xl">
|
<p className="mt-4 text-lg lg:text-2xl">
|
||||||
An app for beer lovers to share their favourite brews and breweries with
|
An app for beer lovers to share their favourite brews and breweries with
|
||||||
like-minded people online.
|
like-minded people online.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import LoginForm from '@/components/Login/LoginForm';
|
import LoginForm from '@/components/Login/LoginForm';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import useRedirectWhenLoggedIn from '@/hooks/useRedirectIfLoggedIn';
|
|||||||
const LoginPage: NextPage = () => {
|
const LoginPage: NextPage = () => {
|
||||||
useRedirectWhenLoggedIn();
|
useRedirectWhenLoggedIn();
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
<meta name="description" content="Login to your account" />
|
<meta name="description" content="Login to your account" />
|
||||||
@@ -49,7 +49,7 @@ const LoginPage: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import RegisterUserForm from '@/components/RegisterUserForm';
|
import RegisterUserForm from '@/components/RegisterUserForm';
|
||||||
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
|
|
||||||
import useRedirectWhenLoggedIn from '@/hooks/useRedirectIfLoggedIn';
|
import useRedirectWhenLoggedIn from '@/hooks/useRedirectIfLoggedIn';
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
@@ -11,7 +10,7 @@ const RegisterUserPage: NextPage = () => {
|
|||||||
useRedirectWhenLoggedIn();
|
useRedirectWhenLoggedIn();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Register User</title>
|
<title>Register User</title>
|
||||||
<meta name="description" content="Register a new user" />
|
<meta name="description" content="Register a new user" />
|
||||||
@@ -24,7 +23,7 @@ const RegisterUserPage: NextPage = () => {
|
|||||||
>
|
>
|
||||||
<RegisterUserForm />
|
<RegisterUserForm />
|
||||||
</FormPageLayout>
|
</FormPageLayout>
|
||||||
</Layout>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Layout from '@/components/ui/Layout';
|
|
||||||
import Spinner from '@/components/ui/Spinner';
|
import Spinner from '@/components/ui/Spinner';
|
||||||
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
import withPageAuthRequired from '@/getServerSideProps/withPageAuthRequired';
|
||||||
import UserContext from '@/contexts/userContext';
|
import UserContext from '@/contexts/userContext';
|
||||||
@@ -18,24 +17,22 @@ const ProtectedPage: NextPage = () => {
|
|||||||
|
|
||||||
const isDesktop = useMediaQuery('(min-width: 768px)');
|
const isDesktop = useMediaQuery('(min-width: 768px)');
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex h-full flex-col items-center justify-center space-y-3 text-center">
|
||||||
<div className="flex h-full flex-col items-center justify-center space-y-3 text-center">
|
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
||||||
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
|
{user && !isLoading && (
|
||||||
{user && !isLoading && (
|
<>
|
||||||
<>
|
<h1 className="text-2xl font-bold lg:text-7xl">
|
||||||
<h1 className="text-2xl font-bold lg:text-7xl">
|
Good {isMorning && 'morning'}
|
||||||
Good {isMorning && 'morning'}
|
{isAfternoon && 'afternoon'}
|
||||||
{isAfternoon && 'afternoon'}
|
{isEvening && 'evening'}
|
||||||
{isEvening && 'evening'}
|
{`, ${user?.firstName}!`}
|
||||||
{`, ${user?.firstName}!`}
|
</h1>
|
||||||
</h1>
|
<h2 className="text-xl font-bold lg:text-4xl">
|
||||||
<h2 className="text-xl font-bold lg:text-4xl">
|
Welcome to the Biergarten App!
|
||||||
Welcome to the Biergarten App!
|
</h2>
|
||||||
</h2>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
7
src/prisma/seed/clean/index.ts
Normal file
7
src/prisma/seed/clean/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import logger from '@/config/pino/logger';
|
||||||
|
import cleanDatabase from './cleanDatabase';
|
||||||
|
|
||||||
|
cleanDatabase().then(() => {
|
||||||
|
logger.info('Database cleaned');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@apply shadow-md card-compact bg-base-300
|
@apply card-compact bg-base-300;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//themes
|
//themes
|
||||||
|
|
||||||
const darkTheme = {
|
const myThemes = {
|
||||||
default: {
|
dark: {
|
||||||
primary: 'hsl(227, 23%, 20%)',
|
primary: 'hsl(227, 23%, 20%)',
|
||||||
secondary: 'hsl(255, 9%, 69%)',
|
secondary: 'hsl(255, 9%, 69%)',
|
||||||
error: 'hsl(9, 52%, 57%)',
|
error: 'hsl(9, 52%, 57%)',
|
||||||
@@ -12,14 +12,11 @@ const darkTheme = {
|
|||||||
warning: 'hsl(50, 98%, 50%)',
|
warning: 'hsl(50, 98%, 50%)',
|
||||||
'primary-content': 'hsl(0, 0%, 98%)',
|
'primary-content': 'hsl(0, 0%, 98%)',
|
||||||
'error-content': 'hsl(0, 0%, 98%)',
|
'error-content': 'hsl(0, 0%, 98%)',
|
||||||
'base-100': 'hsl(190, 4%, 11%)',
|
'base-100': 'hsl(227, 20%, 11%)',
|
||||||
'base-200': 'hsl(190, 4%, 8%)',
|
'base-200': 'hsl(227, 20%, 8%)',
|
||||||
'base-300': 'hsl(190, 4%, 5%)',
|
'base-300': 'hsl(227, 20%, 5%)',
|
||||||
},
|
},
|
||||||
};
|
light: {
|
||||||
|
|
||||||
const pastelTheme = {
|
|
||||||
default: {
|
|
||||||
primary: 'hsl(180, 15%, 60%)',
|
primary: 'hsl(180, 15%, 60%)',
|
||||||
secondary: 'hsl(21, 54%, 83%)',
|
secondary: 'hsl(21, 54%, 83%)',
|
||||||
error: 'hsl(4, 87%, 74%)',
|
error: 'hsl(4, 87%, 74%)',
|
||||||
@@ -30,9 +27,9 @@ const pastelTheme = {
|
|||||||
warning: 'hsl(40, 76%, 73%)',
|
warning: 'hsl(40, 76%, 73%)',
|
||||||
'primary-content': 'hsl(0, 0%, 0%)',
|
'primary-content': 'hsl(0, 0%, 0%)',
|
||||||
'error-content': 'hsl(0, 0%, 0%)',
|
'error-content': 'hsl(0, 0%, 0%)',
|
||||||
'base-100': 'hsl(0, 0%, 94%)',
|
'base-100': 'hsl(180, 8%, 94%)',
|
||||||
'base-200': 'hsl(0, 0%, 90%)',
|
'base-200': 'hsl(180, 8%, 92%)',
|
||||||
'base-300': 'hsl(0, 0%, 85%)',
|
'base-300': 'hsl(180, 8%, 88%)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,10 +47,11 @@ module.exports = {
|
|||||||
require('@headlessui/tailwindcss'),
|
require('@headlessui/tailwindcss'),
|
||||||
require('daisyui'),
|
require('daisyui'),
|
||||||
require('tailwindcss-animate'),
|
require('tailwindcss-animate'),
|
||||||
|
require('autoprefixer'),
|
||||||
],
|
],
|
||||||
|
|
||||||
daisyui: {
|
daisyui: {
|
||||||
logs: false,
|
logs: false,
|
||||||
themes: [darkTheme, pastelTheme],
|
themes: [myThemes],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user