From aa994f0067c0e0d3c236beec21eaa9d61f7a3a8d Mon Sep 17 00:00:00 2001 From: Aaron William Po Date: Sat, 10 Jun 2023 22:09:51 -0400 Subject: [PATCH] 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 =