From 140abaa5a17ebb183b5d7e65e8ead001a9b6a64a Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sun, 4 Jun 2023 13:26:14 -0400 Subject: [PATCH 1/3] update: add delete user api route, AuthProvider extracted from App.tsx --- src/components/Account/DeleteAccount.tsx | 30 +++++++++++--- src/components/Account/Security.tsx | 4 +- src/components/ui/CustomToast.tsx | 2 +- src/contexts/UserContext.ts | 13 ------- src/contexts/UserContext.tsx | 24 ++++++++++++ src/hooks/auth/useUser.ts | 7 +++- src/pages/_app.tsx | 17 ++++---- .../api/users/[id]/{edit.ts => index.ts} | 39 ++++++++++++++++--- src/pages/api/users/edit-password.ts | 16 +------- src/requests/User/sendEditUserRequest.ts | 2 +- .../User/sendUpdatePasswordRequest.ts | 9 +---- src/services/User/deleteUserById.ts | 25 ++++++++++++ 12 files changed, 129 insertions(+), 59 deletions(-) delete mode 100644 src/contexts/UserContext.ts create mode 100644 src/contexts/UserContext.tsx rename src/pages/api/users/[id]/{edit.ts => index.ts} (76%) create mode 100644 src/services/User/deleteUserById.ts diff --git a/src/components/Account/DeleteAccount.tsx b/src/components/Account/DeleteAccount.tsx index 73af80c..b55b33e 100644 --- a/src/components/Account/DeleteAccount.tsx +++ b/src/components/Account/DeleteAccount.tsx @@ -1,7 +1,10 @@ +import UserContext from '@/contexts/UserContext'; import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer'; + import { Switch } from '@headlessui/react'; import { useRouter } from 'next/router'; -import { Dispatch, FunctionComponent, useRef } from 'react'; +import { Dispatch, FunctionComponent, useContext, useRef } from 'react'; +import { toast } from 'react-hot-toast'; interface DeleteAccountProps { pageState: AccountPageState; @@ -13,6 +16,26 @@ const DeleteAccount: FunctionComponent = ({ }) => { const deleteRef = useRef(null); const router = useRouter(); + const { user, mutate } = useContext(UserContext); + + const onDeleteSubmit = async () => { + deleteRef.current!.close(); + const loadingToast = toast.loading( + 'Deleting your account. We are sad to see you go. 😭', + ); + const request = await fetch(`/api/users/${user?.id}`, { + method: 'DELETE', + }); + + if (!request.ok) { + throw new Error('Could not delete that user.'); + } + + toast.remove(loadingToast); + toast.success('Deleted your account. Goodbye. 😓'); + await mutate!(); + router.push('/'); + }; return (
@@ -49,10 +72,7 @@ const DeleteAccount: FunctionComponent = ({
diff --git a/src/components/Account/Security.tsx b/src/components/Account/Security.tsx index 5a03485..4e7ba4a 100644 --- a/src/components/Account/Security.tsx +++ b/src/components/Account/Security.tsx @@ -72,14 +72,14 @@ const Security: FunctionComponent = ({ dispatch, pageState }) => formValidationSchema={register('password')} /> - Confirm Password + Confirm Password {formState.errors.confirmPassword?.message} diff --git a/src/components/ui/CustomToast.tsx b/src/components/ui/CustomToast.tsx index 1ad6822..cc038f7 100644 --- a/src/components/ui/CustomToast.tsx +++ b/src/components/ui/CustomToast.tsx @@ -27,7 +27,7 @@ const CustomToast: FC<{ children: ReactNode }> = ({ children }) => { const alertType = toastToClassName(t.type); return (

{resolveValue(t.message, t)}

{t.type !== 'loading' && ( diff --git a/src/contexts/UserContext.ts b/src/contexts/UserContext.ts deleted file mode 100644 index ac84c98..0000000 --- a/src/contexts/UserContext.ts +++ /dev/null @@ -1,13 +0,0 @@ -import useUser from '@/hooks/auth/useUser'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; -import { createContext } from 'react'; -import { z } from 'zod'; - -const UserContext = createContext<{ - user?: z.infer; - error?: unknown; - isLoading: boolean; - mutate?: ReturnType['mutate']; -}>({ isLoading: true }); - -export default UserContext; diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx new file mode 100644 index 0000000..2cf7213 --- /dev/null +++ b/src/contexts/UserContext.tsx @@ -0,0 +1,24 @@ +import useUser from '@/hooks/auth/useUser'; +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import { ReactNode, createContext } from 'react'; +import { z } from 'zod'; + +const UserContext = createContext<{ + user?: z.infer; + error?: unknown; + isLoading: boolean; + mutate?: ReturnType['mutate']; +}>({ isLoading: true }); + +export default UserContext; + +type AuthProviderComponent = (props: { children: ReactNode }) => JSX.Element; + +export const AuthProvider: AuthProviderComponent = ({ children }) => { + const { error, isLoading, mutate, user } = useUser(); + return ( + + {children} + + ); +}; diff --git a/src/hooks/auth/useUser.ts b/src/hooks/auth/useUser.ts index 86c4a9f..39a98d7 100644 --- a/src/hooks/auth/useUser.ts +++ b/src/hooks/auth/useUser.ts @@ -46,7 +46,12 @@ const useUser = () => { return parsedPayload.data; }); - return { user, isLoading, error: error as unknown, mutate }; + return { + mutate, + isLoading, + user: error ? undefined : user, + error: error as unknown, + }; }; export default useUser; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 86a4e86..64d822b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,4 +1,4 @@ -import UserContext from '@/contexts/UserContext'; +import UserContext, { AuthProvider } from '@/contexts/UserContext'; import '@/styles/globals.css'; import type { AppProps } from 'next/app'; @@ -13,15 +13,12 @@ import Layout from '@/components/ui/Layout'; import useUser from '@/hooks/auth/useUser'; import CustomToast from '@/components/ui/CustomToast'; -const spaceGrotesk = Space_Grotesk({ - subsets: ['latin'], -}); +const spaceGrotesk = Space_Grotesk({ subsets: ['latin'] }); -export default function App({ Component, pageProps }: AppProps) { +const App = ({ Component, pageProps }: AppProps) => { useEffect(() => { themeChange(false); }, []); - const { user, isLoading, error, mutate } = useUser(); return ( <> @@ -38,15 +35,17 @@ export default function App({ Component, pageProps }: AppProps) { content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> - + - + ); -} +}; + +export default App; diff --git a/src/pages/api/users/[id]/edit.ts b/src/pages/api/users/[id]/index.ts similarity index 76% rename from src/pages/api/users/[id]/edit.ts rename to src/pages/api/users/[id]/index.ts index e644878..46f7c0f 100644 --- a/src/pages/api/users/[id]/edit.ts +++ b/src/pages/api/users/[id]/index.ts @@ -4,6 +4,7 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import ServerError from '@/config/util/ServerError'; import DBClient from '@/prisma/DBClient'; +import deleteUserById from '@/services/User/deleteUserById'; import findUserByEmail from '@/services/User/findUserByEmail'; import findUserById from '@/services/User/findUserById'; import findUserByUsername from '@/services/User/findUserByUsername'; @@ -21,11 +22,12 @@ const EditUserSchema = BaseCreateUserSchema.pick({ lastName: true, }); -interface EditUserRequest extends UserExtendedNextApiRequest { +interface UserRouteRequest extends UserExtendedNextApiRequest { + query: { id: string }; +} + +interface EditUserRequest extends UserRouteRequest { body: z.infer; - query: { - id: string; - }; } const checkIfUserCanEditUser = async ( @@ -41,7 +43,7 @@ const checkIfUserCanEditUser = async ( } if (authenticatedUser.id !== userToUpdate.id) { - throw new ServerError('You are not permitted to edit this user', 403); + throw new ServerError('You are not permitted to modify this user', 403); } await next(); @@ -88,6 +90,24 @@ const editUser = async ( }); }; +const deleteUser = async ( + req: UserRouteRequest, + res: NextApiResponse>, +) => { + const { id } = req.query; + const deletedUser = await deleteUserById(id); + + if (!deletedUser) { + throw new ServerError('Could not find a user with that id.', 400); + } + + res.send({ + message: 'Successfully deleted user.', + statusCode: 200, + success: true, + }); +}; + const router = createRouter< EditUserRequest, NextApiResponse> @@ -103,6 +123,15 @@ router.put( editUser, ); +router.delete( + getCurrentUser, + validateRequest({ + querySchema: z.object({ id: z.string().cuid() }), + }), + checkIfUserCanEditUser, + deleteUser, +); + const handler = router.handler(NextConnectOptions); export default handler; diff --git a/src/pages/api/users/edit-password.ts b/src/pages/api/users/edit-password.ts index 5f29441..d62f21c 100644 --- a/src/pages/api/users/edit-password.ts +++ b/src/pages/api/users/edit-password.ts @@ -5,7 +5,6 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser'; import validateRequest from '@/config/nextConnect/middleware/validateRequest'; import DBClient from '@/prisma/DBClient'; import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { NextApiResponse } from 'next'; import { createRouter } from 'next-connect'; @@ -23,26 +22,15 @@ const updatePassword = async ( const hash = await hashPassword(password); const user = req.user!; - const updatedUser: z.infer = await DBClient.instance.user.update({ + await DBClient.instance.user.update({ data: { hash }, where: { id: user.id }, - select: { - id: true, - username: true, - createdAt: true, - updatedAt: true, - email: true, - firstName: true, - lastName: true, - dateOfBirth: true, - accountIsVerified: true, - }, }); + res.json({ message: 'Updated user password.', statusCode: 200, success: true, - payload: updatedUser, }); }; const router = createRouter< diff --git a/src/requests/User/sendEditUserRequest.ts b/src/requests/User/sendEditUserRequest.ts index 0bff563..17eb453 100644 --- a/src/requests/User/sendEditUserRequest.ts +++ b/src/requests/User/sendEditUserRequest.ts @@ -13,7 +13,7 @@ interface SendEditUserRequestArgs { } const sendEditUserRequest = async ({ user, data }: SendEditUserRequestArgs) => { - const response = await fetch(`/api/users/${user!.id}/edit`, { + const response = await fetch(`/api/users/${user!.id}`, { body: JSON.stringify(data), method: 'PUT', headers: { 'Content-Type': 'application/json' }, diff --git a/src/requests/User/sendUpdatePasswordRequest.ts b/src/requests/User/sendUpdatePasswordRequest.ts index 1c17ef6..01b55c3 100644 --- a/src/requests/User/sendUpdatePasswordRequest.ts +++ b/src/requests/User/sendUpdatePasswordRequest.ts @@ -1,5 +1,4 @@ import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; @@ -21,13 +20,7 @@ const sendUpdatePasswordRequest = async (data: z.infer { + const deletedUser: z.infer | null = + await DBClient.instance.user.delete({ + where: { id }, + select: { + id: true, + username: true, + email: true, + firstName: true, + lastName: true, + dateOfBirth: true, + createdAt: true, + accountIsVerified: true, + updatedAt: true, + }, + }); + + return deletedUser; +}; + +export default deleteUserById; From aa994f0067c0e0d3c236beec21eaa9d61f7a3a8d Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 10 Jun 2023 22:09:51 -0400 Subject: [PATCH 2/3] Feat: add create brewery post, brewery image upload Add address autocomplete, using MapBox --- package-lock.json | 192 +++++++++++- package.json | 4 +- src/components/CreateBeerPostForm.tsx | 16 +- src/components/ui/CustomToast.tsx | 11 +- src/components/ui/forms/FormPageLayout.tsx | 2 +- src/components/ui/forms/FormTextInput.tsx | 2 + src/pages/api/beers/[id]/images/index.ts | 4 +- src/pages/api/breweries/[id]/images/index.ts | 86 ++++++ src/pages/api/breweries/create.ts | 76 +++++ src/pages/api/users/register.ts | 5 +- src/pages/breweries/[id]/beers/create.tsx | 1 - src/pages/breweries/create.tsx | 280 ++++++++++++++++++ .../sendUploadBreweryImageRequest.ts | 34 +++ ...ImageDataIntoDB.ts => addBeerImageToDB.ts} | 4 +- .../BreweryImage/addBreweryImageToDB.ts | 39 +++ .../BreweryPost/createNewBreweryPost.ts | 55 ++++ .../types/CreateBreweryPostSchema.ts | 55 ++++ .../schema/CreateUserValidationSchemas.ts | 3 + src/util/withPageAuthRequired.ts | 25 +- 19 files changed, 855 insertions(+), 39 deletions(-) create mode 100644 src/pages/api/breweries/[id]/images/index.ts create mode 100644 src/pages/api/breweries/create.ts create mode 100644 src/pages/breweries/create.tsx create mode 100644 src/requests/BreweryImage/sendUploadBreweryImageRequest.ts rename src/services/BeerImage/{processImageDataIntoDB.ts => addBeerImageToDB.ts} (92%) create mode 100644 src/services/BreweryImage/addBreweryImageToDB.ts create mode 100644 src/services/BreweryPost/createNewBreweryPost.ts create mode 100644 src/services/BreweryPost/types/CreateBreweryPostSchema.ts diff --git a/package-lock.json b/package-lock.json index 06ec697..c24c2fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@headlessui/tailwindcss": "^0.1.3", "@hookform/resolvers": "^3.1.0", "@mapbox/mapbox-sdk": "^0.15.1", + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/search-js-react": "^1.0.0-beta.16", "@next/bundle-analyzer": "^13.4.4", "@prisma/client": "^4.15.0", "@react-email/components": "^0.0.6", @@ -638,6 +640,19 @@ "npm": ">=6.14.13" } }, + "node_modules/@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "node_modules/@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "dependencies": { + "@floating-ui/core": "^0.7.3" + } + }, "node_modules/@hapi/b64": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", @@ -989,6 +1004,74 @@ "polyline": "bin/polyline.bin.js" } }, + "node_modules/@mapbox/search-js-core": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-core/-/search-js-core-1.0.0-beta.16.tgz", + "integrity": "sha512-8Y+9HivlN/yQyEm2AqA3C9WRNiE9j8JVFoG1ZZnt5Rczm5Y+7BubdVH66buUvZxvrhzdyiyQgejGoNLxtrkhfw==", + "dependencies": { + "@types/geojson": "^7946.0.8" + }, + "engines": { + "node": ">=12.20.1" + } + }, + "node_modules/@mapbox/search-js-react": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-react/-/search-js-react-1.0.0-beta.16.tgz", + "integrity": "sha512-PsGU0D0VuoK9YUd70YGoWhaaEdZ851sBn/TLm3Rfu9HTF1ZHE7GemVS9YjmlNSf491VrIuYX3cDVElkq+J04gg==", + "dependencies": { + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/search-js-web": "^1.0.0-beta.16", + "@types/geojson": "^7946.0.8", + "@types/react": "^17.0.43" + }, + "engines": { + "node": ">=12.20.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@mapbox/search-js-react/node_modules/@types/react": { + "version": "17.0.60", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.60.tgz", + "integrity": "sha512-pCH7bqWIfzHs3D+PDs3O/COCQJka+Kcw3RnO9rFA2zalqoXg7cNjJDh6mZ7oRtY1wmY4LVwDdAbA1F7Z8tv3BQ==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@mapbox/search-js-web": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-web/-/search-js-web-1.0.0-beta.16.tgz", + "integrity": "sha512-jCofyJZPnLUZjdEFX6cAS2V4q5RJHPe+M7JSdmnA3bfMEYZwrQdT920vy2Psm9Ep/h7sHwMzGGEtu58DpdmA7w==", + "dependencies": { + "@floating-ui/dom": "^0.5.2", + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/sphericalmercator": "^1.2.0", + "focus-trap": "^6.7.3", + "no-scroll": "^2.1.1" + }, + "engines": { + "node": ">=12.20.1" + }, + "peerDependencies": { + "mapbox-gl": ">=2.7.0" + } + }, + "node_modules/@mapbox/sphericalmercator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.2.0.tgz", + "integrity": "sha512-ZTOuuwGuMOJN+HEmG/68bSEw15HHaMWmQ5gdTsWdWsjDe56K1kGvLOK6bOSC8gWgIvEO0w6un/2Gvv1q5hJSkQ==", + "bin": { + "bbox": "bin/bbox.js", + "to4326": "bin/to4326.js", + "to900913": "bin/to900913.js", + "xyz": "bin/xyz.js" + } + }, "node_modules/@mapbox/tiny-sdf": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", @@ -1973,8 +2056,7 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/qs": { "version": "6.9.7", @@ -2051,8 +2133,7 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "node_modules/@types/semver": { "version": "7.3.13", @@ -5343,6 +5424,14 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/focus-trap": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz", + "integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==", + "dependencies": { + "tabbable": "^5.3.3" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8021,6 +8110,11 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/no-scroll": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/no-scroll/-/no-scroll-2.1.1.tgz", + "integrity": "sha512-YTzGAJOo/B6hkodeT5SKKHpOhAzjMfkUCCXjLJwjWk2F4/InIg+HbdH9kmT7bKpleDuqLZDTRy2OdNtAj0IVyQ==" + }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -10568,6 +10662,11 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, "node_modules/tailwindcss": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", @@ -11899,6 +11998,19 @@ "integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==", "dev": true }, + "@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "requires": { + "@floating-ui/core": "^0.7.3" + } + }, "@hapi/b64": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", @@ -12178,6 +12290,54 @@ "meow": "^6.1.1" } }, + "@mapbox/search-js-core": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-core/-/search-js-core-1.0.0-beta.16.tgz", + "integrity": "sha512-8Y+9HivlN/yQyEm2AqA3C9WRNiE9j8JVFoG1ZZnt5Rczm5Y+7BubdVH66buUvZxvrhzdyiyQgejGoNLxtrkhfw==", + "requires": { + "@types/geojson": "^7946.0.8" + } + }, + "@mapbox/search-js-react": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-react/-/search-js-react-1.0.0-beta.16.tgz", + "integrity": "sha512-PsGU0D0VuoK9YUd70YGoWhaaEdZ851sBn/TLm3Rfu9HTF1ZHE7GemVS9YjmlNSf491VrIuYX3cDVElkq+J04gg==", + "requires": { + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/search-js-web": "^1.0.0-beta.16", + "@types/geojson": "^7946.0.8", + "@types/react": "^17.0.43" + }, + "dependencies": { + "@types/react": { + "version": "17.0.60", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.60.tgz", + "integrity": "sha512-pCH7bqWIfzHs3D+PDs3O/COCQJka+Kcw3RnO9rFA2zalqoXg7cNjJDh6mZ7oRtY1wmY4LVwDdAbA1F7Z8tv3BQ==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + } + } + }, + "@mapbox/search-js-web": { + "version": "1.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@mapbox/search-js-web/-/search-js-web-1.0.0-beta.16.tgz", + "integrity": "sha512-jCofyJZPnLUZjdEFX6cAS2V4q5RJHPe+M7JSdmnA3bfMEYZwrQdT920vy2Psm9Ep/h7sHwMzGGEtu58DpdmA7w==", + "requires": { + "@floating-ui/dom": "^0.5.2", + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/sphericalmercator": "^1.2.0", + "focus-trap": "^6.7.3", + "no-scroll": "^2.1.1" + } + }, + "@mapbox/sphericalmercator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.2.0.tgz", + "integrity": "sha512-ZTOuuwGuMOJN+HEmG/68bSEw15HHaMWmQ5gdTsWdWsjDe56K1kGvLOK6bOSC8gWgIvEO0w6un/2Gvv1q5hJSkQ==" + }, "@mapbox/tiny-sdf": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", @@ -12943,8 +13103,7 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/qs": { "version": "6.9.7", @@ -13020,8 +13179,7 @@ "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" }, "@types/semver": { "version": "7.3.13", @@ -15433,6 +15591,14 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "focus-trap": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz", + "integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==", + "requires": { + "tabbable": "^5.3.3" + } + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -17292,6 +17458,11 @@ "regexparam": "^2.0.1" } }, + "no-scroll": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/no-scroll/-/no-scroll-2.1.1.tgz", + "integrity": "sha512-YTzGAJOo/B6hkodeT5SKKHpOhAzjMfkUCCXjLJwjWk2F4/InIg+HbdH9kmT7bKpleDuqLZDTRy2OdNtAj0IVyQ==" + }, "node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -19064,6 +19235,11 @@ "tslib": "^2.5.0" } }, + "tabbable": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", + "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==" + }, "tailwindcss": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", diff --git a/package.json b/package.json index 143bac4..b1451ec 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@headlessui/tailwindcss": "^0.1.3", "@hookform/resolvers": "^3.1.0", "@mapbox/mapbox-sdk": "^0.15.1", + "@mapbox/search-js-core": "^1.0.0-beta.16", + "@mapbox/search-js-react": "^1.0.0-beta.16", "@next/bundle-analyzer": "^13.4.4", "@prisma/client": "^4.15.0", "@react-email/components": "^0.0.6", @@ -76,11 +78,11 @@ "eslint-config-next": "^13.4.4", "eslint-config-prettier": "^8.8.0", "eslint-plugin-react": "^7.32.2", + "onchange": "^7.1.0", "postcss": "^8.4.24", "prettier": "^2.8.8", "prettier-plugin-jsdoc": "^0.4.2", "prettier-plugin-tailwindcss": "^0.3.0", - "onchange": "^7.1.0", "prisma": "^4.15.0", "tailwindcss": "^3.3.2", "tailwindcss-animate": "^1.0.5", diff --git a/src/components/CreateBeerPostForm.tsx b/src/components/CreateBeerPostForm.tsx index a7ff91c..b0b76df 100644 --- a/src/components/CreateBeerPostForm.tsx +++ b/src/components/CreateBeerPostForm.tsx @@ -34,15 +34,15 @@ const CreateBeerPostForm: FunctionComponent = ({ types = [], brewery, }) => { - const { register, handleSubmit, formState } = useForm< - z.infer - >({ + const { + register, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm>({ resolver: zodResolver(CreateBeerPostWithImagesValidationSchema), defaultValues: { breweryId: brewery.id }, }); - const { errors, isSubmitting } = formState; - const onSubmit: SubmitHandler< z.infer > = async (data) => { @@ -51,9 +51,9 @@ const CreateBeerPostForm: FunctionComponent = ({ } try { - const response = await sendCreateBeerPostRequest(data); - await sendUploadBeerImagesRequest({ beerPost: response, images: data.images }); - await router.push(`/beers/${response.id}`); + const beerPost = await sendCreateBeerPostRequest(data); + await sendUploadBeerImagesRequest({ beerPost, images: data.images }); + await router.push(`/beers/${beerPost.id}`); toast.success('Created beer post.'); } catch (e) { const errorMessage = e instanceof Error ? e.message : 'Something went wrong.'; diff --git a/src/components/ui/CustomToast.tsx b/src/components/ui/CustomToast.tsx index cc038f7..86d3068 100644 --- a/src/components/ui/CustomToast.tsx +++ b/src/components/ui/CustomToast.tsx @@ -22,14 +22,19 @@ const toastToClassName = (toastType: Toast['type']) => { const CustomToast: FC<{ children: ReactNode }> = ({ children }) => { return ( <> - + {(t) => { const alertType = toastToClassName(t.type); return (
-

{resolveValue(t.message, t)}

+

{resolveValue(t.message, t)}

{t.type !== 'loading' && (
+
+ + +
+
+ + ); +}; + +export default CreateBreweryPage; + +export const getServerSideProps: GetServerSideProps = withPageAuthRequired(); diff --git a/src/requests/BreweryImage/sendUploadBreweryImageRequest.ts b/src/requests/BreweryImage/sendUploadBreweryImageRequest.ts new file mode 100644 index 0000000..31f833b --- /dev/null +++ b/src/requests/BreweryImage/sendUploadBreweryImageRequest.ts @@ -0,0 +1,34 @@ +import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult'; +import { z } from 'zod'; + +interface SendUploadBeerImagesRequestArgs { + breweryPost: z.infer; + images: FileList; +} + +const sendUploadBreweryImagesRequest = async ({ + breweryPost, + images, +}: SendUploadBeerImagesRequestArgs) => { + const formData = new FormData(); + + [...images].forEach((file) => { + formData.append('images', file); + }); + + formData.append('caption', `Image of ${breweryPost.name}`); + formData.append('alt', breweryPost.name); + + const uploadResponse = await fetch(`/api/breweries/${breweryPost.id}/images`, { + method: 'POST', + body: formData, + }); + + if (!uploadResponse.ok) { + throw new Error('Failed to upload images'); + } + + return uploadResponse.json(); +}; + +export default sendUploadBreweryImagesRequest; diff --git a/src/services/BeerImage/processImageDataIntoDB.ts b/src/services/BeerImage/addBeerImageToDB.ts similarity index 92% rename from src/services/BeerImage/processImageDataIntoDB.ts rename to src/services/BeerImage/addBeerImageToDB.ts index a59cf2c..8cff61b 100644 --- a/src/services/BeerImage/processImageDataIntoDB.ts +++ b/src/services/BeerImage/addBeerImageToDB.ts @@ -11,7 +11,7 @@ interface ProcessImageDataArgs { userId: string; } -const processImageDataIntoDB = ({ +const addBeerImageToDB = ({ alt, caption, files, @@ -36,4 +36,4 @@ const processImageDataIntoDB = ({ return Promise.all(beerImagePromises); }; -export default processImageDataIntoDB; +export default addBeerImageToDB; diff --git a/src/services/BreweryImage/addBreweryImageToDB.ts b/src/services/BreweryImage/addBreweryImageToDB.ts new file mode 100644 index 0000000..92d7100 --- /dev/null +++ b/src/services/BreweryImage/addBreweryImageToDB.ts @@ -0,0 +1,39 @@ +import DBClient from '@/prisma/DBClient'; +import { BreweryImage } from '@prisma/client'; +import { z } from 'zod'; +import ImageMetadataValidationSchema from '../types/ImageSchema/ImageMetadataValidationSchema'; + +interface ProcessImageDataArgs { + files: Express.Multer.File[]; + alt: z.infer['alt']; + caption: z.infer['caption']; + breweryPostId: string; + userId: string; +} + +const addBreweryImageToDB = ({ + alt, + caption, + files, + breweryPostId, + userId, +}: ProcessImageDataArgs) => { + const breweryImagePromises: Promise[] = []; + files.forEach((file) => { + breweryImagePromises.push( + DBClient.instance.breweryImage.create({ + data: { + alt, + caption, + postedBy: { connect: { id: userId } }, + breweryPost: { connect: { id: breweryPostId } }, + path: file.path, + }, + }), + ); + }); + + return Promise.all(breweryImagePromises); +}; + +export default addBreweryImageToDB; diff --git a/src/services/BreweryPost/createNewBreweryPost.ts b/src/services/BreweryPost/createNewBreweryPost.ts new file mode 100644 index 0000000..c24ed4c --- /dev/null +++ b/src/services/BreweryPost/createNewBreweryPost.ts @@ -0,0 +1,55 @@ +import DBClient from '@/prisma/DBClient'; +import { z } from 'zod'; +import CreateBreweryPostSchema from './types/CreateBreweryPostSchema'; +import BreweryPostQueryResult from './types/BreweryPostQueryResult'; + +const CreateNewBreweryPostWithUserAndLocationSchema = CreateBreweryPostSchema.omit({ + address: true, + city: true, + country: true, + stateOrProvince: true, +}).extend({ + userId: z.string().cuid(), + locationId: z.string().cuid(), +}); + +const createNewBreweryPost = async ({ + dateEstablished, + description, + locationId, + name, + userId, +}: z.infer) => { + const breweryPost: z.infer = + await DBClient.instance.breweryPost.create({ + data: { + name, + description, + dateEstablished, + location: { connect: { id: locationId } }, + postedBy: { connect: { id: userId } }, + }, + select: { + id: true, + name: true, + description: true, + createdAt: true, + dateEstablished: true, + postedBy: { select: { id: true, username: true } }, + breweryImages: { select: { path: true, caption: true, id: true, alt: true } }, + location: { + select: { + city: true, + address: true, + coordinates: true, + country: true, + stateOrProvince: true, + }, + }, + }, + }); + + return breweryPost; +}; + +export default createNewBreweryPost; diff --git a/src/services/BreweryPost/types/CreateBreweryPostSchema.ts b/src/services/BreweryPost/types/CreateBreweryPostSchema.ts new file mode 100644 index 0000000..55b44d4 --- /dev/null +++ b/src/services/BreweryPost/types/CreateBreweryPostSchema.ts @@ -0,0 +1,55 @@ +import { isPast } from 'date-fns'; +import { z } from 'zod'; + +const CreateBreweryPostSchema = z.object({ + name: z + .string({ + required_error: 'Brewery name is required.', + invalid_type_error: 'Brewery name must be a string.', + }) + .min(1, { message: 'Brewery name is required.' }) + .max(100, { message: 'Brewery name is too long.' }), + description: z + .string({ + required_error: 'Description is required.', + invalid_type_error: 'Description must be a string.', + }) + .min(1, { message: 'Description is required.' }) + .max(500, { message: 'Description is too long.' }), + address: z + .string({ + required_error: 'Address is required.', + invalid_type_error: 'Address must be a string.', + }) + .min(1, { message: 'Address is required.' }) + .max(100, { message: 'Address is too long.' }), + + city: z + .string({ + required_error: 'City is required.', + invalid_type_error: 'City must be a string.', + }) + .min(1, { message: 'City is required.' }) + .max(100, { message: 'City is too long.' }), + + region: z + .string({ invalid_type_error: 'region must be a string.' }) + .min(1, { message: 'region is required.' }) + .max(100, { message: 'region is too long.' }) + .optional(), + + country: z + .string({ invalid_type_error: 'Country must be a string.' }) + .max(100, { message: 'Country is too long.' }) + .optional(), + + dateEstablished: z.coerce + .date({ + required_error: 'Date established is required.', + invalid_type_error: 'Date established must be a date string.', + }) + .refine((val) => !Number.isNaN(val.toString()), { message: 'Date is invalid.' }) + .refine((val) => isPast(new Date(val)), { message: 'Date must be in the past.' }), +}); + +export default CreateBreweryPostSchema; diff --git a/src/services/User/schema/CreateUserValidationSchemas.ts b/src/services/User/schema/CreateUserValidationSchemas.ts index 3d1bf12..3d86980 100644 --- a/src/services/User/schema/CreateUserValidationSchemas.ts +++ b/src/services/User/schema/CreateUserValidationSchemas.ts @@ -37,6 +37,9 @@ export const BaseCreateUserSchema = z.object({ }), dateOfBirth: z .string() + .refine((val) => !Number.isNaN(Date.parse(val)), { + message: 'Date is invalid.', + }) .refine((dateOfBirth) => new Date(dateOfBirth) <= MINIMUM_DATE_OF_BIRTH, { message: 'You must be at least 19 years old to register.', }), diff --git a/src/util/withPageAuthRequired.ts b/src/util/withPageAuthRequired.ts index fb3521c..e9171ea 100644 --- a/src/util/withPageAuthRequired.ts +++ b/src/util/withPageAuthRequired.ts @@ -9,13 +9,13 @@ import { getLoginSession } from '../config/auth/session'; * @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. + * the `ParsedUrlQuery` type. * @template D - A generic type that represents preview data. It defaults to the - * PreviewData type. + * `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. + * @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< P extends { [key: string]: any } = { [key: string]: any }, @@ -30,14 +30,15 @@ export type ExtendedGetServerSideProps< * A Higher Order Function that adds an 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. + * @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 = From 77e358e2b52073a66db580bc698eb74c4b17dd8c Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 10 Jun 2023 22:19:45 -0400 Subject: [PATCH 3/3] Fix: downgrade typescript to fix stack overflow error in compiler --- package-lock.json | 16 ++++++++-------- package.json | 2 +- src/pages/breweries/create.tsx | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index c24c2fc..f9f326f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "tailwindcss": "^3.3.2", "tailwindcss-animate": "^1.0.5", "ts-node": "^10.9.1", - "typescript": "^5.1.3" + "typescript": "^4.9.0" } }, "node_modules/@alloc/quick-lru": { @@ -11200,16 +11200,16 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -19621,9 +19621,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "devOptional": true }, "unbox-primitive": { diff --git a/package.json b/package.json index b1451ec..bc3e566 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "tailwindcss": "^3.3.2", "tailwindcss-animate": "^1.0.5", "ts-node": "^10.9.1", - "typescript": "^5.1.3" + "typescript": "^4.9.0" }, "prisma": { "schema": "./src/prisma/schema.prisma", diff --git a/src/pages/breweries/create.tsx b/src/pages/breweries/create.tsx index be6cf11..7e32d09 100644 --- a/src/pages/breweries/create.tsx +++ b/src/pages/breweries/create.tsx @@ -26,7 +26,6 @@ import UploadImageValidationSchema from '@/services/types/ImageSchema/UploadImag import sendUploadBreweryImagesRequest from '@/requests/BreweryImage/sendUploadBreweryImageRequest'; const AddressAutofill = dynamic( - // @ts-expect-error () => import('@mapbox/search-js-react').then((mod) => mod.AddressAutofill), { ssr: false }, );