mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
868
package-lock.json
generated
868
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -13,41 +13,41 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/iron": "^7.0.1",
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@headlessui/tailwindcss": "^0.1.3",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mapbox/mapbox-sdk": "^0.15.1",
|
||||
"@next/bundle-analyzer": "^13.4.3",
|
||||
"@prisma/client": "^4.13.0",
|
||||
"@next/bundle-analyzer": "^13.4.4",
|
||||
"@prisma/client": "^4.15.0",
|
||||
"@react-email/components": "^0.0.6",
|
||||
"@react-email/render": "^0.0.7",
|
||||
"@react-email/tailwind": "^0.0.8",
|
||||
"@vercel/analytics": "^1.0.0",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"argon2": "^0.30.3",
|
||||
"cloudinary": "^1.36.4",
|
||||
"cloudinary": "^1.37.0",
|
||||
"cookie": "^0.5.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv": "^16.1.3",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mapbox-gl": "^2.14.1",
|
||||
"mapbox-gl": "^2.15.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-storage-cloudinary": "^4.0.0",
|
||||
"next": "^13.3.4",
|
||||
"next": "^13.4.4",
|
||||
"next-connect": "^1.0.0-next.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pino": "^8.12.0",
|
||||
"pino": "^8.14.1",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-daisyui": "^3.1.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-email": "^1.9.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-hook-form": "^7.44.3",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-intersection-observer": "^9.4.3",
|
||||
"react-map-gl": "^7.0.23",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-intersection-observer": "^9.4.4",
|
||||
"react-map-gl": "^7.0.25",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"sparkpost": "^2.1.4",
|
||||
"swr": "^2.1.5",
|
||||
@@ -55,37 +55,37 @@
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@types/mapbox__mapbox-sdk": "^0.13.4",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1",
|
||||
"@types/react": "^18.2.8",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/sparkpost": "^2.1.5",
|
||||
"@vercel/fetch": "^6.2.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^2.51.6",
|
||||
"daisyui": "^3.0.0",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-airbnb-base": "15.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-plugin-react": "^7.32.2",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-jsdoc": "^0.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"onchange": "^7.1.0",
|
||||
"prisma": "^4.13.0",
|
||||
"prisma": "^4.15.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"prisma": {
|
||||
"schema": "./src/prisma/schema.prisma",
|
||||
|
||||
@@ -3,19 +3,25 @@ import validateUsernameRequest from '@/requests/validateUsernameRequest';
|
||||
import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
import { Switch } from '@headlessui/react';
|
||||
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 { z } from 'zod';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import sendEditUserRequest from '@/requests/User/sendEditUserRequest';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { AccountPageAction, AccountPageState } from '@/reducers/accountPageReducer';
|
||||
import FormError from '../ui/forms/FormError';
|
||||
import FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
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 EditUserSchema = BaseCreateUserSchema.pick({
|
||||
@@ -47,18 +53,16 @@ const AccountInfo: FC = () => {
|
||||
),
|
||||
});
|
||||
|
||||
const [editToggled, setEditToggled] = useState(false);
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof EditUserSchema>) => {
|
||||
const loadingToast = toast.loading('Submitting edits...');
|
||||
try {
|
||||
await sendEditUserRequest({ user: user!, data });
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Edits submitted successfully.');
|
||||
setEditToggled(false);
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
await mutate!();
|
||||
} catch (error) {
|
||||
setEditToggled(false);
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
toast.remove(loadingToast);
|
||||
createErrorToast(error);
|
||||
await mutate!();
|
||||
@@ -82,9 +86,9 @@ const AccountInfo: FC = () => {
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={editToggled}
|
||||
checked={pageState.accountInfoOpen}
|
||||
onClick={async () => {
|
||||
setEditToggled((val) => !val);
|
||||
dispatch({ type: 'TOGGLE_ACCOUNT_INFO_VISIBILITY' });
|
||||
await mutate!();
|
||||
reset({
|
||||
username: user!.username,
|
||||
@@ -96,7 +100,7 @@ const AccountInfo: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editToggled && (
|
||||
{pageState.accountInfoOpen && (
|
||||
<form
|
||||
className="form-control space-y-5"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
@@ -109,7 +113,7 @@ const AccountInfo: FC = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.username}
|
||||
id="username"
|
||||
formValidationSchema={register('username')}
|
||||
@@ -120,7 +124,7 @@ const AccountInfo: FC = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="email"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.email}
|
||||
id="email"
|
||||
formValidationSchema={register('email')}
|
||||
@@ -134,7 +138,7 @@ const AccountInfo: FC = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.firstName}
|
||||
id="firstName"
|
||||
formValidationSchema={register('firstName')}
|
||||
@@ -147,7 +151,7 @@ const AccountInfo: FC = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="text"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.lastName}
|
||||
id="lastName"
|
||||
formValidationSchema={register('lastName')}
|
||||
@@ -157,7 +161,7 @@ const AccountInfo: FC = () => {
|
||||
<button
|
||||
className="btn-primary btn my-5 w-full"
|
||||
type="submit"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.accountInfoOpen || formState.isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
|
||||
76
src/components/Account/DeleteAccount.tsx
Normal file
76
src/components/Account/DeleteAccount.tsx
Normal 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;
|
||||
@@ -1,17 +1,24 @@
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { Dispatch, FunctionComponent } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||
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 FormInfo from '../ui/forms/FormInfo';
|
||||
import FormLabel from '../ui/forms/FormLabel';
|
||||
import FormTextInput from '../ui/forms/FormTextInput';
|
||||
|
||||
const Security: FunctionComponent = () => {
|
||||
const [editToggled, setEditToggled] = useState(false);
|
||||
interface SecurityProps {
|
||||
pageState: AccountPageState;
|
||||
dispatch: Dispatch<AccountPageAction>;
|
||||
}
|
||||
|
||||
const Security: FunctionComponent<SecurityProps> = ({ dispatch, pageState }) => {
|
||||
const { register, handleSubmit, formState, reset } = useForm<
|
||||
z.infer<typeof UpdatePasswordSchema>
|
||||
>({
|
||||
@@ -19,9 +26,16 @@ const Security: FunctionComponent = () => {
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof UpdatePasswordSchema>> = async (data) => {
|
||||
const loadingToast = toast.loading('Changing password.');
|
||||
try {
|
||||
await sendUpdatePasswordRequest(data);
|
||||
setEditToggled(value => !value)
|
||||
reset();
|
||||
toast.remove(loadingToast);
|
||||
toast.success('Password changed successfully.');
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
} catch (error) {
|
||||
dispatch({ type: 'CLOSE_ALL' });
|
||||
createErrorToast(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -36,15 +50,15 @@ const Security: FunctionComponent = () => {
|
||||
<Switch
|
||||
className="toggle"
|
||||
id="edit-toggle"
|
||||
checked={editToggled}
|
||||
checked={pageState.securityOpen}
|
||||
onClick={() => {
|
||||
setEditToggled((val) => !val);
|
||||
dispatch({ type: 'TOGGLE_SECURITY_VISIBILITY' });
|
||||
reset();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{editToggled && (
|
||||
{pageState.securityOpen && (
|
||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="password">New Password</FormLabel>
|
||||
@@ -52,7 +66,7 @@ const Security: FunctionComponent = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.password}
|
||||
id="password"
|
||||
formValidationSchema={register('password')}
|
||||
@@ -63,7 +77,7 @@ const Security: FunctionComponent = () => {
|
||||
</FormInfo>
|
||||
<FormTextInput
|
||||
type="password"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
error={!!formState.errors.confirmPassword}
|
||||
id="password"
|
||||
formValidationSchema={register('confirmPassword')}
|
||||
@@ -71,7 +85,7 @@ const Security: FunctionComponent = () => {
|
||||
|
||||
<button
|
||||
className="btn-primary btn mt-5"
|
||||
disabled={!editToggled || formState.isSubmitting}
|
||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
|
||||
@@ -121,10 +121,10 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
))}
|
||||
</Rating>
|
||||
</div>
|
||||
<div className="btn-group btn-group-horizontal">
|
||||
<div className="join">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-xs btn lg:btn-sm"
|
||||
className="btn-xs join-item btn lg:btn-sm"
|
||||
disabled={isSubmitting || isDeleting}
|
||||
onClick={() => {
|
||||
setInEditMode(false);
|
||||
@@ -135,13 +135,13 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || isDeleting}
|
||||
className="btn-xs btn lg:btn-sm"
|
||||
className="btn-xs join-item btn lg:btn-sm"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-xs btn lg:btn-sm"
|
||||
className="btn-xs join-item btn lg:btn-sm"
|
||||
onClick={onDelete}
|
||||
disabled={isDeleting || formState.isSubmitting}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -27,9 +27,9 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const alertType = toastToClassName(t.type);
|
||||
return (
|
||||
<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' && (
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -12,11 +12,11 @@ const DesktopLinks: FC = () => {
|
||||
|
||||
return (
|
||||
<div className="block flex-none">
|
||||
<ul className="menu menu-horizontal p-0">
|
||||
<ul className="menu menu-horizontal menu-sm">
|
||||
{pages.map((page) => {
|
||||
return (
|
||||
<li key={page.slug}>
|
||||
<Link tabIndex={0} href={page.slug}>
|
||||
<Link tabIndex={0} href={page.slug} className="hover:bg-primary-focus">
|
||||
<span
|
||||
className={`text-lg uppercase ${
|
||||
currentURL === page.slug ? 'font-black' : 'font-medium'
|
||||
@@ -43,7 +43,7 @@ const MobileLinks: FC = () => {
|
||||
</label>
|
||||
<ul
|
||||
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) => (
|
||||
<li key={page.slug}>
|
||||
@@ -64,7 +64,7 @@ const Navbar = () => {
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
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">
|
||||
<Link className="btn-ghost btn normal-case" href="/">
|
||||
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
|
||||
@@ -98,7 +98,7 @@ const Navbar = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Navbar;
|
||||
|
||||
@@ -14,7 +14,8 @@ const Button: FunctionComponent<FormButtonProps> = ({
|
||||
// eslint-disable-next-line react/button-has-type
|
||||
<button
|
||||
type={type}
|
||||
className={`btn-primary btn w-full rounded-xl ${isSubmitting ? 'loading' : ''}`}
|
||||
className={`btn-primary btn w-full rounded-xl`}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@@ -4,12 +4,21 @@ import { NextPage } from 'next';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import Head from 'next/head';
|
||||
import AccountInfo from '@/components/Account/AccountInfo';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useReducer } from 'react';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import Security from '@/components/Account/Security';
|
||||
import DeleteAccount from '@/components/Account/DeleteAccount';
|
||||
import accountPageReducer from '@/reducers/accountPageReducer';
|
||||
|
||||
const AccountPage: NextPage = () => {
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const [pageState, dispatch] = useReducer(accountPageReducer, {
|
||||
accountInfoOpen: false,
|
||||
securityOpen: false,
|
||||
deleteAccountOpen: false,
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -46,8 +55,9 @@ const AccountPage: NextPage = () => {
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel className="h-full space-y-5">
|
||||
<AccountInfo />
|
||||
<Security />
|
||||
<AccountInfo pageState={pageState} dispatch={dispatch} />
|
||||
<Security pageState={pageState} dispatch={dispatch} />
|
||||
<DeleteAccount pageState={pageState} dispatch={dispatch} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>Your posts!</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
|
||||
3
src/prisma/migrations/20230603010553_/migration.sql
Normal file
3
src/prisma/migrations/20230603010553_/migration.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Location" ADD COLUMN "createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "updatedAt" TIMESTAMPTZ(3);
|
||||
@@ -106,6 +106,8 @@ model Location {
|
||||
postedBy User @relation(fields: [postedById], references: [id], onDelete: Cascade)
|
||||
postedById String
|
||||
BreweryPost BreweryPost?
|
||||
createdAt DateTime @default(now()) @db.Timestamptz(3)
|
||||
updatedAt DateTime? @updatedAt @db.Timestamptz(3)
|
||||
}
|
||||
|
||||
model BreweryPost {
|
||||
|
||||
@@ -22,7 +22,6 @@ const createNewBeerImages = async ({
|
||||
joinData: { beerPosts, users },
|
||||
}: CreateNewBeerImagesArgs) => {
|
||||
const prisma = DBClient.instance;
|
||||
const createdAt = faker.date.past(1);
|
||||
|
||||
const beerImageData: BeerImageData[] = [];
|
||||
|
||||
@@ -33,6 +32,7 @@ const createNewBeerImages = async ({
|
||||
const caption = faker.lorem.sentence();
|
||||
const alt = faker.lorem.sentence();
|
||||
const path = imageUrls[Math.floor(Math.random() * imageUrls.length)];
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
|
||||
beerImageData.push({
|
||||
path,
|
||||
|
||||
@@ -34,7 +34,7 @@ const createNewBeerComments = async ({
|
||||
const content = faker.lorem.lines(5);
|
||||
const user = users[Math.floor(Math.random() * users.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;
|
||||
|
||||
beerCommentData.push({
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
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';
|
||||
|
||||
interface BeerPostLikeData {
|
||||
beerPostId: string;
|
||||
likedById: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const createNewBeerPostLikes = async ({
|
||||
@@ -19,10 +22,11 @@ const createNewBeerPostLikes = async ({
|
||||
for (let i = 0; i < numberOfLikes; i++) {
|
||||
const beerPost = beerPosts[Math.floor(Math.random() * beerPosts.length)];
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
beerPostLikeData.push({
|
||||
beerPostId: beerPost.id,
|
||||
likedById: user.id,
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ const createNewBeerPosts = async ({
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const beerType = beerTypes[Math.floor(Math.random() * beerTypes.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 ibu = Math.floor(Math.random() * (60 - 10) + 10);
|
||||
|
||||
@@ -45,7 +45,7 @@ const createNewBeerTypes = async ({ joinData }: CreateNewBeerTypesArgs) => {
|
||||
|
||||
types.forEach((type) => {
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const createdAt = faker.date.past(1);
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
|
||||
beerTypeData.push({
|
||||
name: type,
|
||||
|
||||
@@ -26,7 +26,7 @@ const createNewBreweryImages = async ({
|
||||
joinData: { breweryPosts, users },
|
||||
}: CreateBreweryImagesArgs) => {
|
||||
const prisma = DBClient.instance;
|
||||
const createdAt = faker.date.past(1);
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
const breweryImageData: BreweryImageData[] = [];
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
||||
@@ -26,13 +26,14 @@ const createNewBreweryPostComments = async ({
|
||||
const { breweryPosts, users } = joinData;
|
||||
const prisma = DBClient.instance;
|
||||
const breweryPostCommentData: BreweryPostCommentData[] = [];
|
||||
const createdAt = faker.date.past(1);
|
||||
const rating = Math.floor(Math.random() * 5) + 1;
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < numberOfComments; i++) {
|
||||
const content = faker.lorem.lines(3).replace(/\n/g, ' ');
|
||||
const user = users[Math.floor(Math.random() * users.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({
|
||||
content,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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';
|
||||
|
||||
interface BreweryPostLikeData {
|
||||
breweryPostId: string;
|
||||
likedById: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const createNewBreweryPostLikes = async ({
|
||||
@@ -21,10 +24,12 @@ const createNewBreweryPostLikes = async ({
|
||||
for (let i = 0; i < numberOfLikes; i++) {
|
||||
const breweryPost = breweryPosts[Math.floor(Math.random() * breweryPosts.length)];
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
|
||||
breweryPostLikeData.push({
|
||||
breweryPostId: breweryPost.id,
|
||||
likedById: user.id,
|
||||
createdAt,
|
||||
});
|
||||
}
|
||||
await DBClient.instance.breweryPostLike.createMany({
|
||||
|
||||
@@ -36,8 +36,8 @@ const createNewBreweryPosts = async ({
|
||||
locations.splice(locationIndex, 1); // Remove the location from the array
|
||||
const description = faker.lorem.lines(20).replace(/(\r\n|\n|\r)/gm, ' ');
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
const createdAt = faker.date.past(1);
|
||||
const dateEstablished = faker.date.past(40);
|
||||
const createdAt = faker.date.past({ years: 1 });
|
||||
const dateEstablished = faker.date.past({ years: 40 });
|
||||
|
||||
breweryData.push({
|
||||
name,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { User } from '@prisma/client';
|
||||
import { GeocodeFeature } from '@mapbox/mapbox-sdk/services/geocoding';
|
||||
import DBClient from '../../DBClient';
|
||||
import geocode from '../../../config/mapbox/geocoder';
|
||||
import canadianCities from '../util/canadianCities';
|
||||
|
||||
interface CreateNewLocationsArgs {
|
||||
numberOfLocations: number;
|
||||
@@ -19,6 +18,7 @@ interface LocationData {
|
||||
coordinates: number[];
|
||||
address: string;
|
||||
postedById: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
const createNewLocations = async ({
|
||||
@@ -27,42 +27,25 @@ const createNewLocations = async ({
|
||||
}: CreateNewLocationsArgs) => {
|
||||
const prisma = DBClient.instance;
|
||||
|
||||
const locationNames: string[] = [];
|
||||
const locationData: LocationData[] = [];
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < numberOfLocations; i++) {
|
||||
locationNames.push(faker.address.cityName());
|
||||
}
|
||||
|
||||
const geocodePromises: Promise<GeocodeFeature>[] = [];
|
||||
|
||||
locationNames.forEach((locationName) => {
|
||||
geocodePromises.push(geocode(locationName));
|
||||
});
|
||||
|
||||
const geocodedLocations = await Promise.all(geocodePromises);
|
||||
|
||||
const locationData: LocationData[] = [];
|
||||
|
||||
geocodedLocations.forEach((geodata) => {
|
||||
const randomIndex = Math.floor(Math.random() * canadianCities.length);
|
||||
const randomCity = canadianCities[randomIndex];
|
||||
const randomUser = joinData.users[Math.floor(Math.random() * joinData.users.length)];
|
||||
|
||||
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;
|
||||
canadianCities.splice(randomIndex, 1);
|
||||
|
||||
locationData.push({
|
||||
city,
|
||||
stateOrProvince,
|
||||
country,
|
||||
coordinates,
|
||||
address,
|
||||
postedById,
|
||||
});
|
||||
address: randomCity.city,
|
||||
city: randomCity.city,
|
||||
coordinates: [randomCity.longitude, randomCity.latitude],
|
||||
createdAt: faker.date.past({ years: 1 }),
|
||||
postedById: randomUser.id,
|
||||
stateOrProvince: randomCity.province,
|
||||
country: 'Canada',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.location.createMany({ data: locationData, skipDuplicates: true });
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < numberOfUsers; i++) {
|
||||
const randomValue = crypto.randomBytes(1).toString('hex');
|
||||
const firstName = faker.name.firstName();
|
||||
const lastName = faker.name.lastName();
|
||||
const firstName = faker.person.firstName();
|
||||
const lastName = faker.person.lastName();
|
||||
const username = `${firstName[0]}.${lastName}.${randomValue}`.toLowerCase();
|
||||
const email = faker.internet
|
||||
.email(firstName, randomValue, 'example.com')
|
||||
.email({ firstName, lastName, provider: 'example.com' })
|
||||
.toLowerCase();
|
||||
|
||||
const userAvailable =
|
||||
@@ -51,7 +51,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
takenEmails.push(email);
|
||||
|
||||
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
||||
const createdAt = faker.date.past(1);
|
||||
const createdAt = faker.date.past({ years: 4 });
|
||||
|
||||
const user = {
|
||||
firstName,
|
||||
|
||||
10425
src/prisma/seed/util/canadianCities.ts
Executable file
10425
src/prisma/seed/util/canadianCities.ts
Executable file
File diff suppressed because it is too large
Load Diff
52
src/reducers/accountPageReducer.ts
Normal file
52
src/reducers/accountPageReducer.ts
Normal 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;
|
||||
@@ -5,6 +5,7 @@ import Welcome from '@/emails/Welcome';
|
||||
import { render } from '@react-email/render';
|
||||
import { z } from 'zod';
|
||||
import { BASE_URL } from '@/config/env';
|
||||
import { ReactElement } from 'react';
|
||||
import GetUserSchema from './schema/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 address = email;
|
||||
|
||||
const html = render(Welcome({ name, url, subject })!);
|
||||
const text = render(Welcome({ name, url, subject })!, { plainText: true });
|
||||
const component = Welcome({ name, url, subject })! as ReactElement<unknown, string>;
|
||||
|
||||
const html = render(component);
|
||||
const text = render(component, { plainText: true });
|
||||
|
||||
await sendEmail({ address, subject, text, html });
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user