Merge pull request #45 from aaronpo97/dev

Dev updates
This commit is contained in:
Aaron Po
2023-06-02 23:15:54 -04:00
committed by GitHub
28 changed files with 11107 additions and 605 deletions

868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,41 +13,41 @@
}, },
"dependencies": { "dependencies": {
"@hapi/iron": "^7.0.1", "@hapi/iron": "^7.0.1",
"@headlessui/react": "^1.7.14", "@headlessui/react": "^1.7.15",
"@headlessui/tailwindcss": "^0.1.3", "@headlessui/tailwindcss": "^0.1.3",
"@hookform/resolvers": "^3.1.0", "@hookform/resolvers": "^3.1.0",
"@mapbox/mapbox-sdk": "^0.15.1", "@mapbox/mapbox-sdk": "^0.15.1",
"@next/bundle-analyzer": "^13.4.3", "@next/bundle-analyzer": "^13.4.4",
"@prisma/client": "^4.13.0", "@prisma/client": "^4.15.0",
"@react-email/components": "^0.0.6", "@react-email/components": "^0.0.6",
"@react-email/render": "^0.0.7", "@react-email/render": "^0.0.7",
"@react-email/tailwind": "^0.0.8", "@react-email/tailwind": "^0.0.8",
"@vercel/analytics": "^1.0.0", "@vercel/analytics": "^1.0.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"cloudinary": "^1.36.4", "cloudinary": "^1.37.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"dotenv": "^16.0.3", "dotenv": "^16.1.3",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mapbox-gl": "^2.14.1", "mapbox-gl": "^2.15.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"multer-storage-cloudinary": "^4.0.0", "multer-storage-cloudinary": "^4.0.0",
"next": "^13.3.4", "next": "^13.4.4",
"next-connect": "^1.0.0-next.3", "next-connect": "^1.0.0-next.3",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pino": "^8.12.0", "pino": "^8.14.1",
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-daisyui": "^3.1.2", "react-daisyui": "^3.1.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-email": "^1.9.3", "react-email": "^1.9.3",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.44.3",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-icons": "^4.8.0", "react-icons": "^4.9.0",
"react-intersection-observer": "^9.4.3", "react-intersection-observer": "^9.4.4",
"react-map-gl": "^7.0.23", "react-map-gl": "^7.0.25",
"react-responsive-carousel": "^3.2.23", "react-responsive-carousel": "^3.2.23",
"sparkpost": "^2.1.4", "sparkpost": "^2.1.4",
"swr": "^2.1.5", "swr": "^2.1.5",
@@ -55,37 +55,37 @@
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^8.0.2",
"@types/cookie": "^0.5.1", "@types/cookie": "^0.5.1",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
"@types/lodash": "^4.14.194", "@types/lodash": "^4.14.195",
"@types/mapbox__mapbox-sdk": "^0.13.4", "@types/mapbox__mapbox-sdk": "^0.13.4",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/node": "^18.16.3", "@types/node": "^20.2.5",
"@types/passport-local": "^1.0.35", "@types/passport-local": "^1.0.35",
"@types/react": "^18.2.0", "@types/react": "^18.2.8",
"@types/react-dom": "^18.2.1", "@types/react-dom": "^18.2.4",
"@types/sparkpost": "^2.1.5", "@types/sparkpost": "^2.1.5",
"@vercel/fetch": "^6.2.0", "@vercel/fetch": "^6.2.0",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"daisyui": "^2.51.6", "daisyui": "^3.0.0",
"dotenv-cli": "^7.2.1", "dotenv-cli": "^7.2.1",
"eslint": "^8.39.0", "eslint": "^8.41.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0", "eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-next": "^13.3.4", "eslint-config-next": "^13.4.4",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"postcss": "^8.4.23", "postcss": "^8.4.24",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-jsdoc": "^0.4.2",
"prettier-plugin-tailwindcss": "^0.2.8", "prettier-plugin-tailwindcss": "^0.3.0",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"prisma": "^4.13.0", "prisma": "^4.15.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.4" "typescript": "^5.1.3"
}, },
"prisma": { "prisma": {
"schema": "./src/prisma/schema.prisma", "schema": "./src/prisma/schema.prisma",

View File

@@ -3,19 +3,25 @@ import validateUsernameRequest from '@/requests/validateUsernameRequest';
import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas'; import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { FC, useContext, useState } from 'react'; import { Dispatch, FC, useContext } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import UserContext from '@/contexts/UserContext'; import UserContext from '@/contexts/UserContext';
import sendEditUserRequest from '@/requests/User/sendEditUserRequest'; import sendEditUserRequest from '@/requests/User/sendEditUserRequest';
import createErrorToast from '@/util/createErrorToast'; import createErrorToast from '@/util/createErrorToast';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { AccountPageAction, AccountPageState } from '@/reducers/accountPageReducer';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel'; import FormLabel from '../ui/forms/FormLabel';
import FormTextInput from '../ui/forms/FormTextInput'; import FormTextInput from '../ui/forms/FormTextInput';
const AccountInfo: FC = () => { interface AccountInfoProps {
pageState: AccountPageState;
dispatch: Dispatch<AccountPageAction>;
}
const AccountInfo: FC<AccountInfoProps> = ({ pageState, dispatch }) => {
const { user, mutate } = useContext(UserContext); const { user, mutate } = useContext(UserContext);
const EditUserSchema = BaseCreateUserSchema.pick({ const EditUserSchema = BaseCreateUserSchema.pick({
@@ -47,18 +53,16 @@ const AccountInfo: FC = () => {
), ),
}); });
const [editToggled, setEditToggled] = useState(false);
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => { const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
const loadingToast = toast.loading('Submitting edits...'); const loadingToast = toast.loading('Submitting edits...');
try { try {
await sendEditUserRequest({ user: user!, data }); await sendEditUserRequest({ user: user!, data });
toast.remove(loadingToast); toast.remove(loadingToast);
toast.success('Edits submitted successfully.'); toast.success('Edits submitted successfully.');
setEditToggled(false); dispatch({ type: 'CLOSE_ALL' });
await mutate!(); await mutate!();
} catch (error) { } catch (error) {
setEditToggled(false); dispatch({ type: 'CLOSE_ALL' });
toast.remove(loadingToast); toast.remove(loadingToast);
createErrorToast(error); createErrorToast(error);
await mutate!(); await mutate!();
@@ -82,9 +86,9 @@ const AccountInfo: FC = () => {
<Switch <Switch
className="toggle" className="toggle"
id="edit-toggle" id="edit-toggle"
checked={editToggled} checked={pageState.accountInfoOpen}
onClick={async () => { onClick={async () => {
setEditToggled((val) => !val); dispatch({ type: 'TOGGLE_ACCOUNT_INFO_VISIBILITY' });
await mutate!(); await mutate!();
reset({ reset({
username: user!.username, username: user!.username,
@@ -96,7 +100,7 @@ const AccountInfo: FC = () => {
/> />
</div> </div>
</div> </div>
{editToggled && ( {pageState.accountInfoOpen && (
<form <form
className="form-control space-y-5" className="form-control space-y-5"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
@@ -109,7 +113,7 @@ const AccountInfo: FC = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="text" type="text"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.accountInfoOpen || formState.isSubmitting}
error={!!formState.errors.username} error={!!formState.errors.username}
id="username" id="username"
formValidationSchema={register('username')} formValidationSchema={register('username')}
@@ -120,7 +124,7 @@ const AccountInfo: FC = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="email" type="email"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.accountInfoOpen || formState.isSubmitting}
error={!!formState.errors.email} error={!!formState.errors.email}
id="email" id="email"
formValidationSchema={register('email')} formValidationSchema={register('email')}
@@ -134,7 +138,7 @@ const AccountInfo: FC = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="text" type="text"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.accountInfoOpen || formState.isSubmitting}
error={!!formState.errors.firstName} error={!!formState.errors.firstName}
id="firstName" id="firstName"
formValidationSchema={register('firstName')} formValidationSchema={register('firstName')}
@@ -147,7 +151,7 @@ const AccountInfo: FC = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="text" type="text"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.accountInfoOpen || formState.isSubmitting}
error={!!formState.errors.lastName} error={!!formState.errors.lastName}
id="lastName" id="lastName"
formValidationSchema={register('lastName')} formValidationSchema={register('lastName')}
@@ -157,7 +161,7 @@ const AccountInfo: FC = () => {
<button <button
className="btn-primary btn my-5 w-full" className="btn-primary btn my-5 w-full"
type="submit" type="submit"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.accountInfoOpen || formState.isSubmitting}
> >
Save Changes Save Changes
</button> </button>

View File

@@ -0,0 +1,76 @@
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
import { Switch } from '@headlessui/react';
import { useRouter } from 'next/router';
import { Dispatch, FunctionComponent, useRef } from 'react';
interface DeleteAccountProps {
pageState: AccountPageState;
dispatch: Dispatch<AccountPageAction>;
}
const DeleteAccount: FunctionComponent<DeleteAccountProps> = ({
dispatch,
pageState,
}) => {
const deleteRef = useRef<null | HTMLDialogElement>(null);
const router = useRouter();
return (
<div className="card w-full space-y-4">
<div className="card-body">
<div className="flex w-full items-center justify-between space-x-5">
<div className="">
<h1 className="text-lg font-bold">Delete Your Account</h1>
<p>Want to leave? Delete your account here.</p>
</div>
<div>
<Switch
className="toggle"
id="edit-toggle"
checked={pageState.deleteAccountOpen}
onClick={() => {
dispatch({ type: 'TOGGLE_DELETE_ACCOUNT_VISIBILITY' });
}}
/>
</div>
</div>
{pageState.deleteAccountOpen && (
<>
<div className="mt-3">
<button
className="btn-primary btn w-full"
onClick={() => deleteRef.current!.showModal()}
>
Delete my account
</button>
<dialog id="delete-modal" className="modal" ref={deleteRef}>
<div className="modal-box text-center">
<h3 className="text-lg font-bold">{`You're about to delete your account.`}</h3>
<p className="">This action is permanent and cannot be reversed.</p>
<div className="modal-action flex-col space-x-0 space-y-3">
<button
className="btn-error btn-sm btn w-full"
onClick={async () => {
deleteRef.current!.close();
await router.replace('/api/users/logout');
}}
>
Okay, delete my account
</button>
<button
className="btn-success btn-sm btn w-full"
onClick={() => deleteRef.current!.close()}
>
Go back
</button>
</div>
</div>
</dialog>
</div>
</>
)}
</div>
</div>
);
};
export default DeleteAccount;

View File

@@ -1,17 +1,24 @@
import { Switch } from '@headlessui/react'; import { Switch } from '@headlessui/react';
import { FunctionComponent, useState } from 'react'; import { Dispatch, FunctionComponent } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest'; import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest';
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import FormError from '../ui/forms/FormError'; import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo'; import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel'; import FormLabel from '../ui/forms/FormLabel';
import FormTextInput from '../ui/forms/FormTextInput'; import FormTextInput from '../ui/forms/FormTextInput';
const Security: FunctionComponent = () => { interface SecurityProps {
const [editToggled, setEditToggled] = useState(false); pageState: AccountPageState;
dispatch: Dispatch<AccountPageAction>;
}
const Security: FunctionComponent<SecurityProps> = ({ dispatch, pageState }) => {
const { register, handleSubmit, formState, reset } = useForm< const { register, handleSubmit, formState, reset } = useForm<
z.infer<typeof UpdatePasswordSchema> z.infer<typeof UpdatePasswordSchema>
>({ >({
@@ -19,9 +26,16 @@ const Security: FunctionComponent = () => {
}); });
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => { const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
await sendUpdatePasswordRequest(data); const loadingToast = toast.loading('Changing password.');
setEditToggled(value => !value) try {
reset(); await sendUpdatePasswordRequest(data);
toast.remove(loadingToast);
toast.success('Password changed successfully.');
dispatch({ type: 'CLOSE_ALL' });
} catch (error) {
dispatch({ type: 'CLOSE_ALL' });
createErrorToast(error);
}
}; };
return ( return (
@@ -36,15 +50,15 @@ const Security: FunctionComponent = () => {
<Switch <Switch
className="toggle" className="toggle"
id="edit-toggle" id="edit-toggle"
checked={editToggled} checked={pageState.securityOpen}
onClick={() => { onClick={() => {
setEditToggled((val) => !val); dispatch({ type: 'TOGGLE_SECURITY_VISIBILITY' });
reset(); reset();
}} }}
/> />
</div> </div>
</div> </div>
{editToggled && ( {pageState.securityOpen && (
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}> <form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
<FormInfo> <FormInfo>
<FormLabel htmlFor="password">New Password</FormLabel> <FormLabel htmlFor="password">New Password</FormLabel>
@@ -52,7 +66,7 @@ const Security: FunctionComponent = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="password" type="password"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.securityOpen || formState.isSubmitting}
error={!!formState.errors.password} error={!!formState.errors.password}
id="password" id="password"
formValidationSchema={register('password')} formValidationSchema={register('password')}
@@ -63,7 +77,7 @@ const Security: FunctionComponent = () => {
</FormInfo> </FormInfo>
<FormTextInput <FormTextInput
type="password" type="password"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.securityOpen || formState.isSubmitting}
error={!!formState.errors.confirmPassword} error={!!formState.errors.confirmPassword}
id="password" id="password"
formValidationSchema={register('confirmPassword')} formValidationSchema={register('confirmPassword')}
@@ -71,7 +85,7 @@ const Security: FunctionComponent = () => {
<button <button
className="btn-primary btn mt-5" className="btn-primary btn mt-5"
disabled={!editToggled || formState.isSubmitting} disabled={!pageState.securityOpen || formState.isSubmitting}
type="submit" type="submit"
> >
Update Update

View File

@@ -121,10 +121,10 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
))} ))}
</Rating> </Rating>
</div> </div>
<div className="btn-group btn-group-horizontal"> <div className="join">
<button <button
type="button" type="button"
className="btn-xs btn lg:btn-sm" className="btn-xs join-item btn lg:btn-sm"
disabled={isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
onClick={() => { onClick={() => {
setInEditMode(false); setInEditMode(false);
@@ -135,13 +135,13 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
<button <button
type="submit" type="submit"
disabled={isSubmitting || isDeleting} disabled={isSubmitting || isDeleting}
className="btn-xs btn lg:btn-sm" className="btn-xs join-item btn lg:btn-sm"
> >
Save Save
</button> </button>
<button <button
type="button" type="button"
className="btn-xs btn lg:btn-sm" className="btn-xs join-item btn lg:btn-sm"
onClick={onDelete} onClick={onDelete}
disabled={isDeleting || formState.isSubmitting} disabled={isDeleting || formState.isSubmitting}
> >

View File

@@ -1,32 +0,0 @@
import Link from 'next/link';
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa';
import { FC } from 'react';
interface PaginationProps {
pageNum: number;
pageCount: number;
}
const BeerIndexPaginationBar: FC<PaginationProps> = ({ pageCount, pageNum }) => {
return (
<div className="btn-group">
<Link
className={`btn ${pageNum === 1 ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum - 1 } }}
scroll={false}
>
<FaArrowLeft />
</Link>
<button className="btn">Page {pageNum}</button>
<Link
className={`btn ${pageNum === pageCount ? 'btn-disabled' : ''}`}
href={{ pathname: '/beers', query: { page_num: pageNum + 1 } }}
scroll={false}
>
<FaArrowRight />
</Link>
</div>
);
};
export default BeerIndexPaginationBar;

View File

@@ -27,9 +27,9 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
const alertType = toastToClassName(t.type); const alertType = toastToClassName(t.type);
return ( return (
<div <div
className={`alert ${alertType} w-11/12 flex-row items-center shadow-lg animate-in fade-in duration-200 lg:w-2/12`} className={`alert ${alertType} flex w-11/12 items-center justify-between shadow-lg animate-in fade-in duration-200 lg:w-4/12`}
> >
<p className='text-sm'>{resolveValue(t.message, t)}</p> <p className="w-full">{resolveValue(t.message, t)}</p>
{t.type !== 'loading' && ( {t.type !== 'loading' && (
<div> <div>
<button <button

View File

@@ -12,11 +12,11 @@ const DesktopLinks: FC = () => {
return ( return (
<div className="block flex-none"> <div className="block flex-none">
<ul className="menu menu-horizontal p-0"> <ul className="menu menu-horizontal menu-sm">
{pages.map((page) => { {pages.map((page) => {
return ( return (
<li key={page.slug}> <li key={page.slug}>
<Link tabIndex={0} href={page.slug}> <Link tabIndex={0} href={page.slug} className="hover:bg-primary-focus">
<span <span
className={`text-lg uppercase ${ className={`text-lg uppercase ${
currentURL === page.slug ? 'font-black' : 'font-medium' currentURL === page.slug ? 'font-black' : 'font-medium'
@@ -43,7 +43,7 @@ const MobileLinks: FC = () => {
</label> </label>
<ul <ul
tabIndex={0} tabIndex={0}
className="dropdown-content menu rounded-box menu-compact mt-3 w-48 bg-base-100 p-2 shadow" className="menu-compact dropdown-content menu rounded-box mt-3 w-48 bg-base-100 p-2 shadow"
> >
{pages.map((page) => ( {pages.map((page) => (
<li key={page.slug}> <li key={page.slug}>
@@ -64,7 +64,7 @@ const Navbar = () => {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
return ( return (
<nav className="navbar sticky top-0 z-50 bg-primary text-primary-content"> <div className="navbar sticky top-0 z-50 bg-primary text-primary-content">
<div className="flex-1"> <div className="flex-1">
<Link className="btn-ghost btn normal-case" href="/"> <Link className="btn-ghost btn normal-case" href="/">
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span> <span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
@@ -98,7 +98,7 @@ const Navbar = () => {
</div> </div>
</div> </div>
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div> <div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
</nav> </div>
); );
}; };
export default Navbar; export default Navbar;

View File

@@ -14,7 +14,8 @@ const Button: FunctionComponent<FormButtonProps> = ({
// eslint-disable-next-line react/button-has-type // eslint-disable-next-line react/button-has-type
<button <button
type={type} type={type}
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`} className={`btn-primary btn w-full rounded-xl`}
disabled={isSubmitting}
> >
{children} {children}
</button> </button>

View File

@@ -4,12 +4,21 @@ import { NextPage } from 'next';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import Head from 'next/head'; import Head from 'next/head';
import AccountInfo from '@/components/Account/AccountInfo'; import AccountInfo from '@/components/Account/AccountInfo';
import { useContext } from 'react'; import { useContext, useReducer } from 'react';
import UserContext from '@/contexts/UserContext'; import UserContext from '@/contexts/UserContext';
import Security from '@/components/Account/Security'; import Security from '@/components/Account/Security';
import DeleteAccount from '@/components/Account/DeleteAccount';
import accountPageReducer from '@/reducers/accountPageReducer';
const AccountPage: NextPage = () => { const AccountPage: NextPage = () => {
const { user } = useContext(UserContext); const { user } = useContext(UserContext);
const [pageState, dispatch] = useReducer(accountPageReducer, {
accountInfoOpen: false,
securityOpen: false,
deleteAccountOpen: false,
});
if (!user) return null; if (!user) return null;
return ( return (
@@ -46,8 +55,9 @@ const AccountPage: NextPage = () => {
</Tab.List> </Tab.List>
<Tab.Panels> <Tab.Panels>
<Tab.Panel className="h-full space-y-5"> <Tab.Panel className="h-full space-y-5">
<AccountInfo /> <AccountInfo pageState={pageState} dispatch={dispatch} />
<Security /> <Security pageState={pageState} dispatch={dispatch} />
<DeleteAccount pageState={pageState} dispatch={dispatch} />
</Tab.Panel> </Tab.Panel>
<Tab.Panel>Your posts!</Tab.Panel> <Tab.Panel>Your posts!</Tab.Panel>
</Tab.Panels> </Tab.Panels>

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Location" ADD COLUMN "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMPTZ(3);

View File

@@ -106,6 +106,8 @@ model Location {
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade) postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
postedById String postedById String
BreweryPost BreweryPost? BreweryPost BreweryPost?
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
} }
model BreweryPost { model BreweryPost {

View File

@@ -22,7 +22,6 @@ const createNewBeerImages = async ({
joinData: { beerPosts, users }, joinData: { beerPosts, users },
}: CreateNewBeerImagesArgs) => { }: CreateNewBeerImagesArgs) => {
const prisma = DBClient.instance; const prisma = DBClient.instance;
const createdAt = faker.date.past(1);
const beerImageData: BeerImageData[] = []; const beerImageData: BeerImageData[] = [];
@@ -33,6 +32,7 @@ const createNewBeerImages = async ({
const caption = faker.lorem.sentence(); const caption = faker.lorem.sentence();
const alt = faker.lorem.sentence(); const alt = faker.lorem.sentence();
const path = imageUrls[Math.floor(Math.random() * imageUrls.length)]; const path = imageUrls[Math.floor(Math.random() * imageUrls.length)];
const createdAt = faker.date.past({ years: 1 });
beerImageData.push({ beerImageData.push({
path, path,

View File

@@ -34,7 +34,7 @@ const createNewBeerComments = async ({
const content = faker.lorem.lines(5); const content = faker.lorem.lines(5);
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 1 });
const rating = Math.floor(Math.random() * 5) + 1; const rating = Math.floor(Math.random() * 5) + 1;
beerCommentData.push({ beerCommentData.push({

View File

@@ -1,10 +1,13 @@
import type { BeerPost, User } from '@prisma/client'; import type { BeerPost, User } from '@prisma/client';
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
interface BeerPostLikeData { interface BeerPostLikeData {
beerPostId: string; beerPostId: string;
likedById: string; likedById: string;
createdAt: Date;
} }
const createNewBeerPostLikes = async ({ const createNewBeerPostLikes = async ({
@@ -19,10 +22,11 @@ const createNewBeerPostLikes = async ({
for (let i = 0; i < numberOfLikes; i++) { for (let i = 0; i < numberOfLikes; i++) {
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)]; const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past({ years: 1 });
beerPostLikeData.push({ beerPostLikeData.push({
beerPostId: beerPost.id, beerPostId: beerPost.id,
likedById: user.id, likedById: user.id,
createdAt,
}); });
} }

View File

@@ -36,7 +36,7 @@ const createNewBeerPosts = async ({
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)]; const beerType = beerTypes[Math.floor(Math.random() * beerTypes.length)];
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 1 });
const abv = Math.floor(Math.random() * (12 - 4) + 4); const abv = Math.floor(Math.random() * (12 - 4) + 4);
const ibu = Math.floor(Math.random() * (60 - 10) + 10); const ibu = Math.floor(Math.random() * (60 - 10) + 10);

View File

@@ -45,7 +45,7 @@ const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
types.forEach((type) => { types.forEach((type) => {
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 1 });
beerTypeData.push({ beerTypeData.push({
name: type, name: type,

View File

@@ -26,7 +26,7 @@ const createNewBreweryImages = async ({
joinData: { breweryPosts, users }, joinData: { breweryPosts, users },
}: CreateBreweryImagesArgs) => { }: CreateBreweryImagesArgs) => {
const prisma = DBClient.instance; const prisma = DBClient.instance;
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 1 });
const breweryImageData: BreweryImageData[] = []; const breweryImageData: BreweryImageData[] = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus

View File

@@ -26,13 +26,14 @@ const createNewBreweryPostComments = async ({
const { breweryPosts, users } = joinData; const { breweryPosts, users } = joinData;
const prisma = DBClient.instance; const prisma = DBClient.instance;
const breweryPostCommentData: BreweryPostCommentData[] = []; const breweryPostCommentData: BreweryPostCommentData[] = [];
const createdAt = faker.date.past(1);
const rating = Math.floor(Math.random() * 5) + 1;
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfComments; i++) { for (let i = 0; i < numberOfComments; i++) {
const content = faker.lorem.lines(3).replace(/\n/g, ' '); const content = faker.lorem.lines(3).replace(/\n/g, ' ');
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
const createdAt = faker.date.past({ years: 1 });
const rating = Math.floor(Math.random() * 5) + 1;
breweryPostCommentData.push({ breweryPostCommentData.push({
content, content,

View File

@@ -1,9 +1,12 @@
import type { BreweryPost, User } from '@prisma/client'; import type { BreweryPost, User } from '@prisma/client';
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
interface BreweryPostLikeData { interface BreweryPostLikeData {
breweryPostId: string; breweryPostId: string;
likedById: string; likedById: string;
createdAt: Date;
} }
const createNewBreweryPostLikes = async ({ const createNewBreweryPostLikes = async ({
@@ -21,10 +24,12 @@ const createNewBreweryPostLikes = async ({
for (let i = 0; i < numberOfLikes; i++) { for (let i = 0; i < numberOfLikes; i++) {
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)]; const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past({ years: 1 });
breweryPostLikeData.push({ breweryPostLikeData.push({
breweryPostId: breweryPost.id, breweryPostId: breweryPost.id,
likedById: user.id, likedById: user.id,
createdAt,
}); });
} }
await DBClient.instance.breweryPostLike.createMany({ await DBClient.instance.breweryPostLike.createMany({

View File

@@ -36,8 +36,8 @@ const createNewBreweryPosts = async ({
locations.splice(locationIndex, 1); // Remove the location from the array locations.splice(locationIndex, 1); // Remove the location from the array
const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' '); const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
const user = users[Math.floor(Math.random() * users.length)]; const user = users[Math.floor(Math.random() * users.length)];
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 1 });
const dateEstablished = faker.date.past(40); const dateEstablished = faker.date.past({ years: 40 });
breweryData.push({ breweryData.push({
name, name,

View File

@@ -1,9 +1,8 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { User } from '@prisma/client'; import { User } from '@prisma/client';
import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding';
import DBClient from '../../DBClient'; import DBClient from '../../DBClient';
import geocode from '../../../config/mapbox/geocoder'; import canadianCities from '../util/canadianCities';
interface CreateNewLocationsArgs { interface CreateNewLocationsArgs {
numberOfLocations: number; numberOfLocations: number;
@@ -19,6 +18,7 @@ interface LocationData {
coordinates: number[]; coordinates: number[];
address: string; address: string;
postedById: string; postedById: string;
createdAt: Date;
} }
const createNewLocations = async ({ const createNewLocations = async ({
@@ -27,42 +27,25 @@ const createNewLocations = async ({
}: CreateNewLocationsArgs) => { }: CreateNewLocationsArgs) => {
const prisma = DBClient.instance; const prisma = DBClient.instance;
const locationNames: string[] = []; const locationData: LocationData[] = [];
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfLocations; i++) { for (let i = 0; i < numberOfLocations; i++) {
locationNames.push(faker.address.cityName()); const randomIndex = Math.floor(Math.random() * canadianCities.length);
} const randomCity = canadianCities[randomIndex];
const geocodePromises: Promise<GeocodeFeature>[] = [];
locationNames.forEach((locationName) => {
geocodePromises.push(geocode(locationName));
});
const geocodedLocations = await Promise.all(geocodePromises);
const locationData: LocationData[] = [];
geocodedLocations.forEach((geodata) => {
const randomUser = joinData.users[Math.floor(Math.random() * joinData.users.length)]; const randomUser = joinData.users[Math.floor(Math.random() * joinData.users.length)];
canadianCities.splice(randomIndex, 1);
const city = geodata.text;
const postedById = randomUser.id;
const stateOrProvince = geodata.context?.find((c) => c.id.startsWith('region'))?.text;
const country = geodata.context?.find((c) => c.id.startsWith('country'))?.text;
const coordinates = geodata.center;
const address = geodata.place_name;
locationData.push({ locationData.push({
city, address: randomCity.city,
stateOrProvince, city: randomCity.city,
country, coordinates: [randomCity.longitude, randomCity.latitude],
coordinates, createdAt: faker.date.past({ years: 1 }),
address, postedById: randomUser.id,
postedById, stateOrProvince: randomCity.province,
country: 'Canada',
}); });
}); }
await prisma.location.createMany({ data: locationData, skipDuplicates: true }); await prisma.location.createMany({ data: locationData, skipDuplicates: true });

View File

@@ -31,11 +31,11 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
for (let i = 0; i < numberOfUsers; i++) { for (let i = 0; i < numberOfUsers; i++) {
const randomValue = crypto.randomBytes(1).toString('hex'); const randomValue = crypto.randomBytes(1).toString('hex');
const firstName = faker.name.firstName(); const firstName = faker.person.firstName();
const lastName = faker.name.lastName(); const lastName = faker.person.lastName();
const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase(); const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase();
const email = faker.internet const email = faker.internet
.email(firstName, randomValue, 'example.com') .email({ firstName, lastName, provider: 'example.com' })
.toLowerCase(); .toLowerCase();
const userAvailable = const userAvailable =
@@ -51,7 +51,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
takenEmails.push(email); takenEmails.push(email);
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 }); const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
const createdAt = faker.date.past(1); const createdAt = faker.date.past({ years: 4 });
const user = { const user = {
firstName, firstName,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
export interface AccountPageState {
accountInfoOpen: boolean;
securityOpen: boolean;
deleteAccountOpen: boolean;
}
export type AccountPageAction =
| { type: 'TOGGLE_ACCOUNT_INFO_VISIBILITY' }
| { type: 'TOGGLE_SECURITY_VISIBILITY' }
| { type: 'TOGGLE_DELETE_ACCOUNT_VISIBILITY' }
| { type: 'CLOSE_ALL' };
const accountPageReducer = (
state: AccountPageState,
action: AccountPageAction,
): AccountPageState => {
switch (action.type) {
case 'TOGGLE_ACCOUNT_INFO_VISIBILITY': {
return {
accountInfoOpen: !state.accountInfoOpen,
securityOpen: false,
deleteAccountOpen: false,
};
}
case 'TOGGLE_DELETE_ACCOUNT_VISIBILITY': {
return {
accountInfoOpen: false,
securityOpen: false,
deleteAccountOpen: !state.deleteAccountOpen,
};
}
case 'TOGGLE_SECURITY_VISIBILITY': {
return {
accountInfoOpen: false,
securityOpen: !state.securityOpen,
deleteAccountOpen: false,
};
}
case 'CLOSE_ALL': {
return {
accountInfoOpen: false,
securityOpen: false,
deleteAccountOpen: false,
};
}
default: {
throw new Error('Invalid action type.');
}
}
};
export default accountPageReducer;

View File

@@ -5,6 +5,7 @@ import Welcome from '@/emails/Welcome';
import { render } from '@react-email/render'; import { render } from '@react-email/render';
import { z } from 'zod'; import { z } from 'zod';
import { BASE_URL } from '@/config/env'; import { BASE_URL } from '@/config/env';
import { ReactElement } from 'react';
import GetUserSchema from './schema/GetUserSchema'; import GetUserSchema from './schema/GetUserSchema';
type UserSchema = z.infer<typeof GetUserSchema>; type UserSchema = z.infer<typeof GetUserSchema>;
@@ -17,8 +18,10 @@ const sendConfirmationEmail = async ({ id, username, email }: UserSchema) => {
const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`; const url = `${BASE_URL}/users/confirm?token=${confirmationToken}`;
const address = email; const address = email;
const html = render(Welcome({ name, url, subject })!); const component = Welcome({ name, url, subject })! as ReactElement<unknown, string>;
const text = render(Welcome({ name, url, subject })!, { plainText: true });
const html = render(component);
const text = render(component, { plainText: true });
await sendEmail({ address, subject, text, html }); await sendEmail({ address, subject, text, html });
}; };

View File

@@ -1,3 +1,4 @@
{ {
"buildCommand": "npx prisma generate && npx prisma migrate deploy && next build" "buildCommand": "npx prisma generate && npx prisma migrate deploy && next build",
"installCommand": "npm install --legacy-peer-deps"
} }