Style updates

This commit is contained in:
Aaron William Po
2023-04-21 23:32:18 -04:00
parent 2dfb080d0c
commit 6a00532f75
22 changed files with 360 additions and 314 deletions

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,
}) => {
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">
{beerRecommendations.map((beerPost) => (
<div key={beerPost.id} className="w-full">

View File

@@ -1,23 +1,10 @@
import UserContext from '@/contexts/userContext';
import useBeerPostComments from '@/hooks/useBeerPostComments';
import useTimeDistance from '@/hooks/useTimeDistance';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import BeerCommentValidationSchema from '@/services/BeerComment/schema/CreateBeerCommentValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import format from 'date-fns/format';
import Link from 'next/link';
import { Dispatch, FC, SetStateAction, useContext, useEffect, useState } from 'react';
import { Rating } from 'react-daisyui';
import { SubmitHandler, useForm } from 'react-hook-form';
import { FaEllipsisH } from 'react-icons/fa';
import { FC, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { z } from 'zod';
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';
import CommentContentBody from './CommentContentBody';
import EditCommentBody from './EditCommentBody';
interface CommentCardProps {
comment: z.infer<typeof BeerCommentQueryResult>;
@@ -25,269 +12,17 @@ interface CommentCardProps {
ref?: ReturnType<typeof useInView>['ref'];
}
interface CommentCardDropdownProps extends CommentCardProps {
inEditMode: boolean;
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>
);
};
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="flex">
<div className="w-4/12">
<button
type="submit"
disabled={formState.isSubmitting || isDeleting}
className="btn-ghost btn-sm btn w-full"
>
Save
</button>
</div>
<div className="w-4/12">
<button
type="button"
className="btn-ghost btn-sm btn w-full"
disabled={formState.isSubmitting || isDeleting}
onClick={() => {
setInEditMode(false);
}}
>
Cancel
</button>
</div>
<div className="w-4/12">
<button
type="button"
className="btn-ghost btn-sm btn w-full"
onClick={handleDelete}
disabled={isDeleting || formState.isSubmitting}
>
Delete
</button>
</div>
</div>
</div>
</div>
</form>
</div>
);
};
const CommentContentBody: FC<CommentCardDropdownProps> = ({
comment,
ref,
mutate,
inEditMode,
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}
mutate={mutate}
inEditMode={inEditMode}
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>
);
};
const CommentCardBody: FC<CommentCardProps> = ({ comment, mutate, ref }) => {
const [inEditMode, setInEditMode] = useState(false);
return !inEditMode ? (
<CommentContentBody
comment={comment}
inEditMode={inEditMode}
mutate={mutate}
ref={ref}
setInEditMode={setInEditMode}
/>
<CommentContentBody comment={comment} ref={ref} setInEditMode={setInEditMode} />
) : (
<EditCommentBody
comment={comment}
inEditMode={inEditMode}
mutate={mutate}
ref={ref}
setInEditMode={setInEditMode}
ref={ref}
/>
);
};

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

@@ -86,8 +86,12 @@ const Navbar = () => {
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
</Link>
</div>
<div
className="tooltip tooltip-left"
data-tip={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
>
<div>
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>{' '}
{theme === 'light' ? (
<button
className="btn-ghost btn-md btn-circle btn"
@@ -108,6 +112,8 @@ const Navbar = () => {
</button>
)}
</div>
</div>
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
</nav>
);
};

View File

@@ -8,8 +8,8 @@ interface ErrorAlertProps {
const ErrorAlert: FC<ErrorAlertProps> = ({ error, setError }) => {
return (
<div className="alert alert-error shadow-lg">
<div>
<div className="alert alert-error flex-row shadow-lg">
<div className="space-x-1">
<FiAlertTriangle className="h-6 w-6" />
<span>{error}</span>
</div>

View File

@@ -11,7 +11,7 @@ interface FormLabelProps {
*/
const FormLabel: FunctionComponent<FormLabelProps> = ({ htmlFor, children }) => (
<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}
>
{children}

View File

@@ -19,7 +19,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
backLinkText,
}) => {
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="tooltip tooltip-right" data-tip={backLinkText}>
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
@@ -28,7 +28,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
</div>
<div className="flex flex-col items-center space-y-1">
{headingIcon({ className: 'text-4xl' })}{' '}
<h1 className="text-3xl font-bold">{headingText}</h1>
<h1 className="text-center text-3xl font-bold">{headingText}</h1>
</div>
<div className="mt-3">{FormComponent}</div>
</div>

View File

@@ -40,7 +40,7 @@ const FormTextArea: FunctionComponent<FormTextAreaProps> = ({
<textarea
id={id}
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' : ''
}`}
{...formValidationSchema}

View File

@@ -46,7 +46,7 @@ const FormTextInput: FunctionComponent<FormInputProps> = ({
id={id}
type={type}
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' : ''
}`}
{...formValidationSchema}

View File

@@ -11,9 +11,11 @@ const NotFound: NextPage = () => {
<title>404 Page Not Found</title>
<meta name="description" content="404 Page Not Found" />
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-4">
<h1 className="text-7xl font-bold">Error: 404</h1>
<h2 className="text-xl font-bold">Page Not Found</h2>
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
<h1 className="text-3xl font-bold lg:text-5xl">404: Not Found</h1>
<h2 className="text-lg font-bold">
Sorry, the page you are looking for does not exist.
</h2>
</div>
</Layout>
);

View File

@@ -9,9 +9,11 @@ const ServerErrorPage: NextPage = () => {
<title>500 Internal Server Error</title>
<meta name="description" content="500 Internal Server Error" />
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-4">
<h1 className="text-7xl font-bold">Error: 500</h1>
<h2 className="text-xl font-bold">Internal Server Error</h2>
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
<h1 className="text-2xl font-bold lg:text-4xl">500: Something Went Wrong</h1>
<h2 className="text-lg font-bold">
Please try again later or contact us if the problem persists.
</h2>
</div>
</Layout>
);

View File

@@ -2,6 +2,8 @@ import UserContext from '@/contexts/userContext';
import useUser from '@/hooks/useUser';
import '@/styles/globals.css';
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import { themeChange } from 'theme-change';
import { Space_Grotesk } from 'next/font/google';
@@ -10,6 +12,9 @@ const spaceGrotesk = Space_Grotesk({
});
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
themeChange(false);
}, []);
const { user, isLoading, error, mutate } = useUser();
return (

View File

@@ -22,6 +22,10 @@ export default function Document() {
href="favicon/favicon-16x16.png"
/>
<link rel="manifest" href="favicon/site.webmanifest" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
/>
</Head>
<body>
<Main />

View File

@@ -16,7 +16,7 @@ interface EditPageProps {
}
const EditPage: NextPage<EditPageProps> = ({ beerPost }) => {
const pageTitle = `Edit "${beerPost.name}"`;
const pageTitle = `Edit \u201c${beerPost.name}\u201d`;
return (
<Layout>

View File

@@ -55,12 +55,12 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
src={image.path}
height={1080}
width={1920}
className="h-[42rem] w-full object-cover"
className="h-96 w-full object-cover lg:h-[42rem]"
/>
</div>
))
: 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>
@@ -79,7 +79,7 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost, beerRecommendations }
</div>
) : (
<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">
Comments
</Tab>

View File

@@ -38,9 +38,9 @@ const BeerPage: NextPage = () => {
<meta name="description" content="Beer posts" />
</Head>
<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">
<h1 className="text-6xl font-bold">The Biergarten Index</h1>
<h1 className="text-4xl font-bold lg:text-6xl">The Biergarten Index</h1>
{!!user && (
<div
className="tooltip tooltip-left h-full"

View File

@@ -16,7 +16,7 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
brewery,
}) => {
return (
<div className="card bg-base-300" key={brewery.id}>
<div className="card" key={brewery.id}>
<figure className="card-image h-96">
{brewery.breweryImages.length > 0 && (
<Image

View File

@@ -7,7 +7,10 @@ const Home: NextPage = () => {
<>
<Head>
<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>
<Layout>
<div className="flex h-full w-full items-center justify-center bg-primary">

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