Merge pull request #29 from aaronpo97/dev

Feature: Edit beer comments, dark/light mode switch
This commit is contained in:
Aaron Po
2023-04-22 22:47:52 -04:00
committed by GitHub
35 changed files with 635 additions and 307 deletions

23
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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">

View File

@@ -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>
); );
}; };

View 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;

View 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;

View 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;

View File

@@ -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>
); );

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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
View 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;

View File

@@ -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> </>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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>
</> </>
); );

View File

@@ -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>
); );
}; };

View 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;

View File

@@ -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;

View File

@@ -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> </>
); );
}; };

View File

@@ -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> </>
</> </>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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>
</> </>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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> </>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -0,0 +1,7 @@
import logger from '@/config/pino/logger';
import cleanDatabase from './cleanDatabase';
cleanDatabase().then(() => {
logger.info('Database cleaned');
process.exit(0);
});

View File

@@ -3,5 +3,5 @@
@tailwind utilities; @tailwind utilities;
.card { .card {
@apply shadow-md card-compact bg-base-300 @apply card-compact bg-base-300;
} }

View File

@@ -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],
}, },
}; };