Styling changes and refactor

Switch google fonts to use Next.js font optimization, animate comment fade in, and refactor beer like handler and comment submit handler.
This commit is contained in:
Aaron William Po
2023-04-04 20:50:00 -04:00
parent a4362a531c
commit 796a5fce3f
15 changed files with 148 additions and 77 deletions

View File

@@ -10,6 +10,7 @@ import { z } from 'zod';
import { KeyedMutator } from 'swr'; import { KeyedMutator } from 'swr';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { useRouter } from 'next/router';
import Button from '../ui/forms/Button'; import Button from '../ui/forms/Button';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
@@ -44,6 +45,7 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
reset({ rating: 0, content: '' }); reset({ rating: 0, content: '' });
}, [reset]); }, [reset]);
const router = useRouter();
const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async ( const onSubmit: SubmitHandler<z.infer<typeof BeerCommentValidationSchema>> = async (
data, data,
) => { ) => {
@@ -55,44 +57,58 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
beerPostId: beerPost.id, beerPostId: beerPost.id,
}); });
reset(); reset();
await mutate();
const submitTasks: Promise<unknown>[] = [
router.push(`/beers/${beerPost.id}`, undefined, { scroll: false }),
mutate(),
];
await Promise.all(submitTasks);
}; };
const { errors } = formState; const { errors } = formState;
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<FormInfo> <div>
<FormLabel htmlFor="content">Leave a comment</FormLabel> <FormInfo>
<FormError>{errors.content?.message}</FormError> <FormLabel htmlFor="content">Leave a comment</FormLabel>
</FormInfo> <FormError>{errors.content?.message}</FormError>
<FormSegment> </FormInfo>
<FormTextArea <FormSegment>
id="content" <FormTextArea
formValidationSchema={register('content')} id="content"
placeholder="Comment" formValidationSchema={register('content')}
rows={5} placeholder="Comment"
error={!!errors.content?.message} rows={5}
/> error={!!errors.content?.message}
</FormSegment> disabled={formState.isSubmitting}
<FormInfo> />
<FormLabel htmlFor="rating">Rating</FormLabel> </FormSegment>
<FormError>{errors.rating?.message}</FormError> <FormInfo>
</FormInfo> <FormLabel htmlFor="rating">Rating</FormLabel>
<Rating <FormError>{errors.rating?.message}</FormError>
value={rating} </FormInfo>
onChange={(value) => { <Rating
setRating(value); value={rating}
setValue('rating', value); onChange={(value) => {
}} setRating(value);
> setValue('rating', value);
<Rating.Item name="rating-1" className="mask mask-star" /> }}
<Rating.Item name="rating-1" className="mask mask-star" /> >
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
<Rating.Item name="rating-1" className="mask mask-star" /> <Rating.Item name="rating-1" className="mask mask-star" />
</Rating> <Rating.Item name="rating-1" className="mask mask-star" />
<Button type="submit">Submit</Button> <Rating.Item name="rating-1" className="mask mask-star" />
</Rating>
</div>
<div>
<Button type="submit" isSubmitting={formState.isSubmitting}>
Submit
</Button>
</div>
</form> </form>
); );
}; };

View File

@@ -3,6 +3,8 @@ import Link from 'next/link';
import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult'; import beerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
import { z } from 'zod'; import { z } from 'zod';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
interface BeerCommentsPaginationBarProps { interface BeerCommentsPaginationBarProps {
commentsPageNum: number; commentsPageNum: number;
commentsPageCount: number; commentsPageCount: number;
@@ -15,9 +17,9 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
beerPost, beerPost,
}) => ( }) => (
<div className="flex items-center justify-center" id="comments-pagination"> <div className="flex items-center justify-center" id="comments-pagination">
<div className="btn-group grid w-6/12 grid-cols-2"> <div className="btn-group">
<Link <Link
className={`btn-outline btn ${ className={`btn btn-ghost ${
commentsPageNum === 1 commentsPageNum === 1
? 'btn-disabled pointer-events-none' ? 'btn-disabled pointer-events-none'
: 'pointer-events-auto' : 'pointer-events-auto'
@@ -28,10 +30,11 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
}} }}
scroll={false} scroll={false}
> >
Next Comments <FaArrowLeft />
</Link> </Link>
<button className="btn btn-ghost pointer-events-none">{commentsPageNum}</button>
<Link <Link
className={`btn-outline btn ${ className={`btn btn-ghost ${
commentsPageNum === commentsPageCount commentsPageNum === commentsPageCount
? 'btn-disabled pointer-events-none' ? 'btn-disabled pointer-events-none'
: 'pointer-events-auto' : 'pointer-events-auto'
@@ -42,7 +45,7 @@ const BeerCommentsPaginationBar: FC<BeerCommentsPaginationBarProps> = ({
}} }}
scroll={false} scroll={false}
> >
Previous Comments <FaArrowRight />
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -29,6 +29,7 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
pageNum, pageNum,
pageSize, pageSize,
}); });
return ( return (
<div className="w-full space-y-3 md:w-[60%]"> <div className="w-full space-y-3 md:w-[60%]">
<div className="card h-96 bg-base-300"> <div className="card h-96 bg-base-300">
@@ -43,7 +44,7 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
</div> </div>
</div> </div>
{comments && !!commentsPageCount && !isLoading && ( {comments && !!comments.length && !!commentsPageCount && !isLoading && (
<div className="card bg-base-300 pb-6"> <div className="card bg-base-300 pb-6">
{comments.map((comment) => ( {comments.map((comment) => (
<CommentCardBody key={comment.id} comment={comment} mutate={mutate} /> <CommentCardBody key={comment.id} comment={comment} mutate={mutate} />
@@ -60,10 +61,16 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
{!comments?.length && !isLoading && <NoCommentsCard />} {!comments?.length && !isLoading && <NoCommentsCard />}
{isLoading && ( {isLoading && (
<div className="card bg-base-300"> <div className="card bg-base-300 pb-6">
{Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: pageSize }).map((_, i) => (
<CommentLoadingCardBody key={i} /> <CommentLoadingCardBody key={i} />
))} ))}
<BeerCommentsPaginationBar
commentsPageNum={pageNum}
commentsPageCount={20}
beerPost={beerPost}
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,6 +1,6 @@
import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost'; import useCheckIfUserLikesBeerPost from '@/hooks/useCheckIfUserLikesBeerPost';
import sendLikeRequest from '@/requests/sendLikeRequest'; import sendLikeRequest from '@/requests/sendLikeRequest';
import { FC, useState } from 'react'; import { FC, useEffect, useState } from 'react';
import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa'; import { FaThumbsUp, FaRegThumbsUp } from 'react-icons/fa';
import { KeyedMutator } from 'swr'; import { KeyedMutator } from 'swr';
@@ -9,14 +9,18 @@ const BeerPostLikeButton: FC<{
mutateCount: KeyedMutator<number>; mutateCount: KeyedMutator<number>;
}> = ({ beerPostId, mutateCount }) => { }> = ({ beerPostId, mutateCount }) => {
const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId); const { isLiked, mutate: mutateLikeStatus } = useCheckIfUserLikesBeerPost(beerPostId);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(false);
}, [isLiked]);
const handleLike = async () => { const handleLike = async () => {
try { try {
setLoading(true); setLoading(true);
await sendLikeRequest(beerPostId); await sendLikeRequest(beerPostId);
mutateCount(); await mutateCount();
mutateLikeStatus(); await mutateLikeStatus();
setLoading(false); setLoading(false);
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);

View File

@@ -10,7 +10,7 @@ const BeerRecommendations: FunctionComponent<BeerRecommendationsProps> = ({
}) => { }) => {
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 bg-base-300">
<div className="card-body"> <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">
<div> <div>

View File

@@ -1,7 +1,7 @@
import UserContext from '@/contexts/userContext'; import UserContext from '@/contexts/userContext';
import useTimeDistance from '@/hooks/useTimeDistance'; import useTimeDistance from '@/hooks/useTimeDistance';
import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult'; import BeerCommentQueryResult from '@/services/BeerComment/schema/BeerCommentQueryResult';
import { format } from 'date-fns'; import format from 'date-fns/format';
import Link from 'next/link'; import Link from 'next/link';
import { useContext } from 'react'; import { useContext } from 'react';
import { Rating } from 'react-daisyui'; import { Rating } from 'react-daisyui';
@@ -69,7 +69,7 @@ const CommentCardBody: React.FC<{
const timeDistance = useTimeDistance(new Date(comment.createdAt)); const timeDistance = useTimeDistance(new Date(comment.createdAt));
return ( return (
<div className="card-body h-64"> <div className="card-body h-64 animate-in fade-in-10">
<div className="flex flex-col justify-between sm:flex-row"> <div className="flex flex-col justify-between sm:flex-row">
<div> <div>
<h3 className="font-semibold sm:text-2xl"> <h3 className="font-semibold sm:text-2xl">

View File

@@ -1,12 +1,12 @@
const CommentLoadingCardBody = () => { const CommentLoadingCardBody = () => {
return ( return (
<div className="card-body h-64"> <div className="animate card-body h-64 fade-in-10">
<div className="flex animate-pulse space-x-4"> <div className="flex animate-pulse space-x-4 slide-in-from-top">
<div className="flex-1 space-y-4 py-1"> <div className="flex-1 space-y-4 py-1">
<div className="h-4 w-3/4 rounded bg-base-200" /> <div className="h-4 w-3/4 rounded bg-base-100" />
<div className="space-y-2"> <div className="space-y-2">
<div className="h-4 rounded bg-base-200" /> <div className="h-4 rounded bg-base-100" />
<div className="h-4 w-5/6 rounded bg-base-200" /> <div className="h-4 w-5/6 rounded bg-base-100" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { FC } from 'react'; import { FC } from 'react';
interface PaginationProps { interface PaginationProps {
@@ -15,7 +15,7 @@ const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) =>
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }} href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
scroll={false} scroll={false}
> >
« <FaArrowLeft />
</Link> </Link>
<button className="btn">Page {pageNum}</button> <button className="btn">Page {pageNum}</button>
<Link <Link
@@ -23,7 +23,7 @@ const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) =>
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }} href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
scroll={false} scroll={false}
> >
» <FaArrowRight />
</Link> </Link>
</div> </div>
); );

View File

@@ -4,10 +4,10 @@ import Navbar from './Navbar';
const Layout: FC<{ children: ReactNode }> = ({ children }) => { const Layout: FC<{ children: ReactNode }> = ({ children }) => {
return ( return (
<div className="flex h-screen flex-col"> <div className="flex h-screen flex-col">
<header className="top-0"> <header className="sticky top-0 z-50">
<Navbar /> <Navbar />
</header> </header>
<div className="animate-in fade-in top-0 h-full flex-1">{children}</div> <div className="relative top-0 h-full flex-1">{children}</div>
</div> </div>
); );
}; };

View File

@@ -2,6 +2,21 @@ import { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from
import { ParsedUrlQuery } from 'querystring'; import { ParsedUrlQuery } from 'querystring';
import { getLoginSession } from '../config/auth/session'; import { getLoginSession } from '../config/auth/session';
/**
* Represents a type definition for a function that handles server-side rendering with
* extended capabilities.
*
* @template P - A generic type that represents an object with string keys and any values.
* It defaults to an empty object.
* @template Q - A generic type that represents a parsed URL query object. It defaults to
* the ParsedUrlQuery type.
* @template D - A generic type that represents preview data. It defaults to the
* PreviewData type.
* @param context - The context object containing information about the incoming HTTP
* request.
* @param session - An awaited value of the return type of the getLoginSession function.
* @returns - A promise that resolves to the result of the server-side rendering process.
*/
export type ExtendedGetServerSideProps< export type ExtendedGetServerSideProps<
P extends { [key: string]: any } = { [key: string]: any }, P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery, Q extends ParsedUrlQuery = ParsedUrlQuery,
@@ -11,6 +26,20 @@ export type ExtendedGetServerSideProps<
session: Awaited<ReturnType<typeof getLoginSession>>, session: Awaited<ReturnType<typeof getLoginSession>>,
) => Promise<GetServerSidePropsResult<P>>; ) => Promise<GetServerSidePropsResult<P>>;
/**
* A Higher Order Function that adds authentication requirement to a Next.js server-side
* page component.
*
* @param fn An async function that receives the GetServerSidePropsContext and
* authenticated session as arguments and returns a GetServerSidePropsResult with props
* for the wrapped component.
* @returns A promise that resolves to a GetServerSidePropsResult object with props for
* the wrapped component. If authentication is successful, the GetServerSidePropsResult
* will include props generated by the wrapped component's getServerSideProps method. If
* authentication fails, the GetServerSidePropsResult will include a redirect to the
* login page.
*/
const withPageAuthRequired = const withPageAuthRequired =
<P extends { [key: string]: any } = { [key: string]: any }>( <P extends { [key: string]: any } = { [key: string]: any }>(
fn?: ExtendedGetServerSideProps<P>, fn?: ExtendedGetServerSideProps<P>,

View File

@@ -40,7 +40,7 @@ const useBeerPostComments = ({ pageNum, id, pageSize }: UseBeerPostCommentsProps
throw new Error(parsedPayload.error.message); throw new Error(parsedPayload.error.message);
} }
const pageCount = Math.ceil(parseInt(count as string, 10) / 10); const pageCount = Math.ceil(parseInt(count as string, 10) / pageSize);
return { comments: parsedPayload.data, pageCount }; return { comments: parsedPayload.data, pageCount };
}, },
); );

View File

@@ -69,6 +69,7 @@
"prettier-plugin-tailwindcss": "^0.2.3", "prettier-plugin-tailwindcss": "^0.2.3",
"prisma": "^4.10.1", "prisma": "^4.10.1",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"tailwindcss-animate": "^1.0.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.5" "typescript": "^4.9.5"
} }

View File

@@ -3,12 +3,28 @@ 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 { Roboto } from 'next/font/google';
const roboto = Roboto({
weight: ['100', '300', '400', '500', '700', '900'],
subsets: ['latin'],
});
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const { user, isLoading, error } = useUser(); const { user, isLoading, error } = useUser();
return ( return (
<UserContext.Provider value={{ user, isLoading, error }}> <>
<Component {...pageProps} /> <style jsx global>
</UserContext.Provider> {`
html {
font-family: ${roboto.style.fontFamily};
}
`}
</style>
<UserContext.Provider value={{ user, isLoading, error }}>
<Component {...pageProps} />
</UserContext.Provider>
</>
); );
} }

View File

@@ -1,11 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Exo+2:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
html {
font-family: 'Exo 2', sans-serif;
}
}

View File

@@ -9,26 +9,29 @@ module.exports = {
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [require('daisyui')], plugins: [
require('daisyui'),
require('tailwindcss-animate')
],
daisyui: { daisyui: {
logs: false, logs: false,
themes: [ themes: [
{ {
default: { default: {
primary: 'hsl(227, 46%, 25%)', primary: 'hsl(227, 23%, 20%)',
secondary: 'hsl(47, 100%, 80%)', secondary: '#ABA9C3',
error: '#c17c74',
accent: '#fe3bd9', accent: '#fe3bd9',
neutral: '#131520', neutral: '#131520',
info: '#0A7CFF', info: '#0A7CFF',
success: '#8ACE2B', success: '#8ACE2B',
warning: '#F9D002', warning: '#F9D002',
error: '#CF1259',
'primary-content': '#FAF9F6', 'primary-content': '#FAF9F6',
'error-content': '#FAF9F6', 'error-content': '#FAF9F6',
'base-100': 'hsl(190, 4%, 11%)', 'base-100': 'hsl(190, 4%, 9%)',
'base-200': 'hsl(190, 4%, 9%)', 'base-200': 'hsl(190, 4%, 8%)',
'base-300': 'hsl(190, 4%, 8%)', 'base-300': 'hsl(190, 4%, 5%)',
}, },
}, },
], ],