diff --git a/next.config.js b/next.config.js index 79a0359..39d2b47 100644 --- a/next.config.js +++ b/next.config.js @@ -5,5 +5,8 @@ const nextConfig = { domains: ['picsum.photos', 'res.cloudinary.com'], }, }; +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}); -module.exports = nextConfig; +module.exports = withBundleAnalyzer(nextConfig); diff --git a/package-lock.json b/package-lock.json index ba81f1f..1cfce31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@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", "@react-email/components": "^0.0.6", "@react-email/render": "^0.0.7", @@ -996,6 +997,14 @@ "node": ">=6.0.0" } }, + "node_modules/@next/bundle-analyzer": { + "version": "13.4.3", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.3.tgz", + "integrity": "sha512-jzWk6eaCFaIXfIswyQQWnR6FN22HpWoSWe3nLa3JCNkNd2ksriJgn86oQyZRxgAPaEbVKQXBp8GZi8e5DrhVJg==", + "dependencies": { + "webpack-bundle-analyzer": "4.7.0" + } + }, "node_modules/@next/env": { "version": "13.3.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.3.4.tgz", @@ -1352,6 +1361,11 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + }, "node_modules/@prisma/client": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", @@ -2382,7 +2396,6 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -2432,7 +2445,6 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -4162,6 +4174,11 @@ "node": ">=12" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -5779,6 +5796,20 @@ "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -7807,6 +7838,14 @@ "node": ">=4" } }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8281,6 +8320,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -10032,6 +10079,19 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, + "node_modules/sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10648,6 +10708,14 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -11294,6 +11362,36 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "dependencies": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -11390,6 +11488,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xregexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", @@ -12050,6 +12168,14 @@ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==" }, + "@next/bundle-analyzer": { + "version": "13.4.3", + "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-13.4.3.tgz", + "integrity": "sha512-jzWk6eaCFaIXfIswyQQWnR6FN22HpWoSWe3nLa3JCNkNd2ksriJgn86oQyZRxgAPaEbVKQXBp8GZi8e5DrhVJg==", + "requires": { + "webpack-bundle-analyzer": "4.7.0" + } + }, "@next/env": { "version": "13.3.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-13.3.4.tgz", @@ -12272,6 +12398,11 @@ "tslib": "^2.5.0" } }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + }, "@prisma/client": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.13.0.tgz", @@ -13113,8 +13244,7 @@ "acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "devOptional": true + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" }, "acorn-jsx": { "version": "5.3.2", @@ -13148,8 +13278,7 @@ "acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "devOptional": true + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" }, "agent-base": { "version": "6.0.2", @@ -14373,6 +14502,11 @@ "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", "dev": true }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", @@ -15628,6 +15762,14 @@ "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" }, + "gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "requires": { + "duplexer": "^0.1.2" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -17009,6 +17151,11 @@ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -17328,6 +17475,11 @@ "is-wsl": "^2.2.0" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -18528,6 +18680,16 @@ } } }, + "sirv": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", + "integrity": "sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ==", + "requires": { + "@polka/url": "^1.0.0-next.20", + "mrmime": "^1.0.0", + "totalist": "^1.0.0" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -18995,6 +19157,11 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "optional": true }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -19457,6 +19624,29 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-j9b8ynpJS4K+zfO5GGwsAcQX4ZHpWV+yRiHDiL+bE0XHJ8NiPYLTNVQdlFYWxtpg9lfAQNlwJg16J9AJtFSXRg==", + "requires": { + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "gzip-size": "^6.0.0", + "lodash": "^4.17.20", + "opener": "^1.5.2", + "sirv": "^1.0.7", + "ws": "^7.3.1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -19532,6 +19722,12 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + }, "xregexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", diff --git a/package.json b/package.json index 7f1b4a2..ae633c8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@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", "@react-email/components": "^0.0.6", "@react-email/render": "^0.0.7", diff --git a/src/components/Account/AccountInfo.tsx b/src/components/Account/AccountInfo.tsx index 52ac646..954d873 100644 --- a/src/components/Account/AccountInfo.tsx +++ b/src/components/Account/AccountInfo.tsx @@ -1,24 +1,23 @@ -import validateEmail from '@/requests/valdiateEmail'; -import validateUsername from '@/requests/validateUsername'; +import validateEmailRequest from '@/requests/User/validateEmailRequest'; +import validateUsernameRequest from '@/requests/validateUsernameRequest'; import { BaseCreateUserSchema } from '@/services/User/schema/CreateUserValidationSchemas'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; import { Switch } from '@headlessui/react'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter } from 'next/router'; -import { FC, useState } from 'react'; +import { FC, useContext, useState } 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 FormError from '../ui/forms/FormError'; import FormInfo from '../ui/forms/FormInfo'; import FormLabel from '../ui/forms/FormLabel'; import FormTextInput from '../ui/forms/FormTextInput'; -interface AccountInfoProps { - user: z.infer; -} +const AccountInfo: FC = () => { + const { user, mutate } = useContext(UserContext); -const AccountInfo: FC = ({ user }) => { - const router = useRouter(); const EditUserSchema = BaseCreateUserSchema.pick({ username: true, email: true, @@ -30,8 +29,8 @@ const AccountInfo: FC = ({ user }) => { .email({ message: 'Email must be a valid email address.' }) .refine( async (email) => { - if (user.email === email) return true; - return validateEmail(email); + if (user!.email === email) return true; + return validateEmailRequest(email); }, { message: 'Email is already taken.' }, ), @@ -41,8 +40,8 @@ const AccountInfo: FC = ({ user }) => { .max(20, { message: 'Username must be less than 20 characters.' }) .refine( async (username) => { - if (user.username === username) return true; - return validateUsername(username); + if (user!.username === username) return true; + return validateUsernameRequest(username); }, { message: 'Username is already taken.' }, ), @@ -53,29 +52,29 @@ const AccountInfo: FC = ({ user }) => { >({ resolver: zodResolver(EditUserSchema), defaultValues: { - username: user.username, - email: user.email, - firstName: user.firstName, - lastName: user.lastName, + username: user!.username, + email: user!.email, + firstName: user!.firstName, + lastName: user!.lastName, }, }); const [inEditMode, setInEditMode] = useState(false); const onSubmit = async (data: z.infer) => { - const response = await fetch(`/api/users/${user.id}/edit`, { - body: JSON.stringify(data), - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - }); - - if (!response.ok) { - throw new Error('Something went wrong.'); + const loadingToast = toast.loading('Submitting edits...'); + try { + await sendEditUserRequest({ user: user!, data }); + await mutate!(); + setInEditMode(false); + toast.remove(loadingToast); + toast.success('Edits submitted successfully.'); + } catch (error) { + setInEditMode(false); + toast.remove(loadingToast); + createErrorToast(error); + await mutate!(); } - - await response.json(); - - router.reload(); }; return ( diff --git a/src/components/BeerBreweryComments/CommentContentBody.tsx b/src/components/BeerBreweryComments/CommentContentBody.tsx index 9d6a41a..f0e7bf1 100644 --- a/src/components/BeerBreweryComments/CommentContentBody.tsx +++ b/src/components/BeerBreweryComments/CommentContentBody.tsx @@ -2,7 +2,8 @@ import UserContext from '@/contexts/UserContext'; import useTimeDistance from '@/hooks/utilities/useTimeDistance'; import { format } from 'date-fns'; import { Dispatch, FC, SetStateAction, useContext } from 'react'; -import { Link, Rating } from 'react-daisyui'; +import { Rating } from 'react-daisyui'; +import Link from 'next/link'; import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import { z } from 'zod'; diff --git a/src/components/BeerBreweryComments/EditCommentBody.tsx b/src/components/BeerBreweryComments/EditCommentBody.tsx index 4b3938b..704bc3d 100644 --- a/src/components/BeerBreweryComments/EditCommentBody.tsx +++ b/src/components/BeerBreweryComments/EditCommentBody.tsx @@ -1,5 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { FC, useState, Dispatch, SetStateAction, useContext } from 'react'; +import { FC, useState, Dispatch, SetStateAction } from 'react'; import { Rating } from 'react-daisyui'; import { useForm, SubmitHandler } from 'react-hook-form'; import { z } from 'zod'; @@ -7,7 +7,8 @@ import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPost import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments'; -import ToastContext from '@/contexts/ToastContext'; +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'; @@ -31,7 +32,6 @@ interface EditCommentBodyProps { const EditCommentBody: FC = ({ comment, setInEditMode, - mutate, handleDeleteRequest, handleEditRequest, @@ -43,25 +43,41 @@ const EditCommentBody: FC = ({ resolver: zodResolver(CreateCommentValidationSchema), }); - const { toast } = useContext(ToastContext); - const { errors } = formState; + const { errors, isSubmitting } = formState; const [isDeleting, setIsDeleting] = useState(false); const onDelete = async () => { + const loadingToast = toast.loading('Deleting comment...'); setIsDeleting(true); - await handleDeleteRequest(comment.id); - await mutate(); + try { + await handleDeleteRequest(comment.id); + await mutate(); + toast.remove(loadingToast); + toast.success('Deleted comment.'); + } catch (error) { + toast.remove(loadingToast); + createErrorToast(error); + } }; const onEdit: SubmitHandler> = async ( data, ) => { - setInEditMode(true); - await handleEditRequest(comment.id, data); - await mutate(); - toast.success('Submitted edits'); - setInEditMode(false); + const loadingToast = toast.loading('Submitting comment edits...'); + + try { + setInEditMode(true); + await handleEditRequest(comment.id, data); + await mutate(); + toast.remove(loadingToast); + toast.success('Comment edits submitted successfully.'); + setInEditMode(false); + } catch (error) { + toast.remove(loadingToast); + createErrorToast(error); + setInEditMode(false); + } }; return ( @@ -79,7 +95,7 @@ const EditCommentBody: FC = ({ placeholder="Comment" rows={2} error={!!errors.content?.message} - disabled={formState.isSubmitting || isDeleting} + disabled={isSubmitting || isDeleting} />
@@ -98,8 +114,8 @@ const EditCommentBody: FC = ({ ))} @@ -109,7 +125,7 @@ const EditCommentBody: FC = ({ -
+
+

{resolveValue(t.message, t)}

+ {t.type !== 'loading' && ( +
+ +
+ )}
); }} {children} - + ); }; export default CustomToast; diff --git a/src/components/ui/alerts/ErrorAlert.tsx b/src/components/ui/alerts/ErrorAlert.tsx deleted file mode 100644 index b19b922..0000000 --- a/src/components/ui/alerts/ErrorAlert.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Dispatch, FC, SetStateAction } from 'react'; -import { FiAlertTriangle } from 'react-icons/fi'; - -interface ErrorAlertProps { - error: string; - setError: Dispatch>; -} - -const ErrorAlert: FC = ({ error, setError }) => { - return ( -
-
- - {error} -
- -
- -
-
- ); -}; - -export default ErrorAlert; diff --git a/src/contexts/ToastContext.ts b/src/contexts/ToastContext.ts deleted file mode 100644 index e5b5c5e..0000000 --- a/src/contexts/ToastContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createContext } from 'react'; -import toast from 'react-hot-toast'; - -const ToastContext = createContext<{ - toast: typeof toast; -}>({ toast }); - -export default ToastContext; diff --git a/src/hooks/utilities/useMediaQuery.ts b/src/hooks/utilities/useMediaQuery.ts index 60e8413..f27af5a 100644 --- a/src/hooks/utilities/useMediaQuery.ts +++ b/src/hooks/utilities/useMediaQuery.ts @@ -18,8 +18,9 @@ import { useState, useEffect } from 'react'; */ const useMediaQuery = (query: `(${string})`) => { /** - * Initialize the matches state variable to false. This is updated whenever the viewport - * size changes (i.e. when the component is mounted and when the window is resized) + * Initialize the matches state variable to false. This is updated whenever the + * viewport size changes (i.e. when the component is mounted and when the window is + * resized) */ const [matches, setMatches] = useState(false); @@ -34,8 +35,8 @@ const useMediaQuery = (query: `(${string})`) => { } /** - * Add a resize event listener to the window object, and update the `matches` state - * variable whenever the viewport size changes. + * Add a resize event listener to the window object, and update the `matches` + * state variable whenever the viewport size changes. */ const listener = () => setMatches(media.matches); window.addEventListener('resize', listener); diff --git a/src/pages/account/index.tsx b/src/pages/account/index.tsx index bebc7d1..eb45097 100644 --- a/src/pages/account/index.tsx +++ b/src/pages/account/index.tsx @@ -3,16 +3,14 @@ import { NextPage } from 'next'; import { Tab } from '@headlessui/react'; import Head from 'next/head'; -import GetUserSchema from '@/services/User/schema/GetUserSchema'; -import { z } from 'zod'; -import DBClient from '@/prisma/DBClient'; import AccountInfo from '@/components/Account/AccountInfo'; +import { useContext } from 'react'; +import UserContext from '@/contexts/UserContext'; -interface AccountPageProps { - user: z.infer; -} +const AccountPage: NextPage = () => { + const { user } = useContext(UserContext); + if (!user) return null; -const AccountPage: NextPage = ({ user }) => { return ( <> @@ -30,7 +28,7 @@ const AccountPage: NextPage = ({ user }) => {
-

Hello, {user.username}!

+

Hello, {user!.username}!

Welcome to your account page.

@@ -50,7 +48,7 @@ const AccountPage: NextPage = ({ user }) => { - + Content 3 @@ -64,30 +62,4 @@ const AccountPage: NextPage = ({ user }) => { export default AccountPage; -export const getServerSideProps = withPageAuthRequired(async (context, session) => { - const { id } = session; - - const user: z.infer | null = - await DBClient.instance.user.findUnique({ - where: { id }, - select: { - username: true, - email: true, - accountIsVerified: true, - firstName: true, - lastName: true, - dateOfBirth: true, - id: true, - createdAt: true, - }, - }); - - if (!user) { - return { redirect: { destination: '/login', permanent: false } }; - } - return { - props: { - user: JSON.parse(JSON.stringify(user)), - }, - }; -}); +export const getServerSideProps = withPageAuthRequired(); diff --git a/src/requests/sendCreateBeerCommentRequest.ts b/src/requests/BeerComment/sendCreateBeerCommentRequest.ts similarity index 100% rename from src/requests/sendCreateBeerCommentRequest.ts rename to src/requests/BeerComment/sendCreateBeerCommentRequest.ts diff --git a/src/requests/sendUploadBeerImageRequest.ts b/src/requests/BeerImage/sendUploadBeerImageRequest.ts similarity index 100% rename from src/requests/sendUploadBeerImageRequest.ts rename to src/requests/BeerImage/sendUploadBeerImageRequest.ts diff --git a/src/requests/BeerPost/deleteBeerPostRequest.ts b/src/requests/BeerPost/deleteBeerPostRequest.ts new file mode 100644 index 0000000..62c7666 --- /dev/null +++ b/src/requests/BeerPost/deleteBeerPostRequest.ts @@ -0,0 +1,22 @@ +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; + +const deleteBeerPostRequest = async (id: string) => { + const response = await fetch(`/api/beers/${id}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error(response.statusText); + } + + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + + if (!parsed.success) { + throw new Error('Could not successfully parse the response.'); + } + + return parsed; +}; + +export default deleteBeerPostRequest; diff --git a/src/requests/sendBeerPostLikeRequest.ts b/src/requests/BeerPost/sendBeerPostLikeRequest.ts similarity index 100% rename from src/requests/sendBeerPostLikeRequest.ts rename to src/requests/BeerPost/sendBeerPostLikeRequest.ts diff --git a/src/requests/sendCreateBeerPostRequest.ts b/src/requests/BeerPost/sendCreateBeerPostRequest.ts similarity index 100% rename from src/requests/sendCreateBeerPostRequest.ts rename to src/requests/BeerPost/sendCreateBeerPostRequest.ts diff --git a/src/requests/sendEditBeerPostRequest.ts b/src/requests/BeerPost/sendEditBeerPostRequest.ts similarity index 100% rename from src/requests/sendEditBeerPostRequest.ts rename to src/requests/BeerPost/sendEditBeerPostRequest.ts diff --git a/src/requests/BreweryComment/sendCreateBreweryCommentRequest.ts b/src/requests/BreweryComment/sendCreateBreweryCommentRequest.ts new file mode 100644 index 0000000..8312d32 --- /dev/null +++ b/src/requests/BreweryComment/sendCreateBreweryCommentRequest.ts @@ -0,0 +1,39 @@ +import CommentQueryResult from '@/services/types/CommentSchema/CommentQueryResult'; +import CreateCommentValidationSchema from '@/services/types/CommentSchema/CreateCommentValidationSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +const BreweryCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({ + breweryPostId: z.string(), +}); + +const sendCreateBreweryCommentRequest = async ({ + content, + rating, + breweryPostId, +}: z.infer) => { + const response = await fetch(`/api/breweries/${breweryPostId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content, rating }), + }); + + if (!response.ok) { + throw new Error(response.statusText); + } + + const data = await response.json(); + const parsedResponse = APIResponseValidationSchema.safeParse(data); + if (!parsedResponse.success) { + throw new Error('Invalid API response'); + } + + const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload); + if (!parsedPayload.success) { + throw new Error('Invalid API response payload'); + } + + return parsedPayload.data; +}; + +export default sendCreateBreweryCommentRequest; diff --git a/src/requests/sendBreweryPostLikeRequest.ts b/src/requests/BreweryPostLike/sendBreweryPostLikeRequest.ts similarity index 100% rename from src/requests/sendBreweryPostLikeRequest.ts rename to src/requests/BreweryPostLike/sendBreweryPostLikeRequest.ts diff --git a/src/requests/User/sendEditUserRequest.ts b/src/requests/User/sendEditUserRequest.ts new file mode 100644 index 0000000..0bff563 --- /dev/null +++ b/src/requests/User/sendEditUserRequest.ts @@ -0,0 +1,35 @@ +import GetUserSchema from '@/services/User/schema/GetUserSchema'; +import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; +import { z } from 'zod'; + +interface SendEditUserRequestArgs { + user: z.infer; + data: { + username: string; + email: string; + firstName: string; + lastName: string; + }; +} + +const sendEditUserRequest = async ({ user, data }: SendEditUserRequestArgs) => { + const response = await fetch(`/api/users/${user!.id}/edit`, { + body: JSON.stringify(data), + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + }); + if (!response.ok) { + throw new Error(response.statusText); + } + + const json = await response.json(); + + const parsed = APIResponseValidationSchema.safeParse(json); + if (!parsed.success) { + throw new Error('API response validation failed.'); + } + + return parsed; +}; + +export default sendEditUserRequest; diff --git a/src/requests/sendLoginUserRequest.ts b/src/requests/User/sendLoginUserRequest.ts similarity index 100% rename from src/requests/sendLoginUserRequest.ts rename to src/requests/User/sendLoginUserRequest.ts diff --git a/src/requests/sendRegisterUserRequest.ts b/src/requests/User/sendRegisterUserRequest.ts similarity index 100% rename from src/requests/sendRegisterUserRequest.ts rename to src/requests/User/sendRegisterUserRequest.ts diff --git a/src/requests/valdiateEmail.ts b/src/requests/User/validateEmailRequest.ts similarity index 85% rename from src/requests/valdiateEmail.ts rename to src/requests/User/validateEmailRequest.ts index dbd8fc0..4e87291 100644 --- a/src/requests/valdiateEmail.ts +++ b/src/requests/User/validateEmailRequest.ts @@ -1,7 +1,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; -const validateEmail = async (email: string) => { +const validateEmailRequest = async (email: string) => { const response = await fetch(`/api/users/check-email?email=${email}`); const json = await response.json(); @@ -22,4 +22,4 @@ const validateEmail = async (email: string) => { return !parsedPayload.data.emailIsTaken; }; -export default validateEmail; +export default validateEmailRequest; diff --git a/src/requests/validateUsername.ts b/src/requests/validateUsernameRequest.ts similarity index 84% rename from src/requests/validateUsername.ts rename to src/requests/validateUsernameRequest.ts index 264c010..85be206 100644 --- a/src/requests/validateUsername.ts +++ b/src/requests/validateUsernameRequest.ts @@ -1,7 +1,7 @@ import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema'; import { z } from 'zod'; -const validateUsername = async (username: string) => { +const validateUsernameRequest = async (username: string) => { const response = await fetch(`/api/users/check-username?username=${username}`); const json = await response.json(); @@ -22,4 +22,4 @@ const validateUsername = async (username: string) => { return !parsedPayload.data.usernameIsTaken; }; -export default validateUsername; +export default validateUsernameRequest; diff --git a/src/services/BeerPost/schema/BeerRecommendationQueryResult.ts b/src/services/BeerPost/schema/BeerRecommendationQueryResult.ts deleted file mode 100644 index 3f57c49..0000000 --- a/src/services/BeerPost/schema/BeerRecommendationQueryResult.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BeerPost } from '@prisma/client'; - -type BeerRecommendationQueryResult = BeerPost & { - brewery: { - id: string; - name: string; - }; - beerImages: { - id: string; - alt: string; - url: string; - }[]; -}; - -export default BeerRecommendationQueryResult; diff --git a/src/services/User/schema/CreateUserValidationSchemas.ts b/src/services/User/schema/CreateUserValidationSchemas.ts index 1488673..f9b421d 100644 --- a/src/services/User/schema/CreateUserValidationSchemas.ts +++ b/src/services/User/schema/CreateUserValidationSchemas.ts @@ -1,5 +1,5 @@ -import validateEmail from '@/requests/valdiateEmail'; -import validateUsername from '@/requests/validateUsername'; +import validateEmailRequest from '@/requests/User/validateEmailRequest'; +import validateUsernameRequest from '@/requests/validateUsernameRequest'; import sub from 'date-fns/sub'; import { z } from 'zod'; @@ -55,14 +55,14 @@ export const CreateUserValidationSchemaWithUsernameAndEmailCheck = email: z .string() .email({ message: 'Email must be a valid email address.' }) - .refine(async (email) => validateEmail(email), { + .refine(async (email) => validateEmailRequest(email), { message: 'Email is already taken.', }), username: z .string() .min(1, { message: 'Username must not be empty.' }) .max(20, { message: 'Username must be less than 20 characters.' }) - .refine(async (username) => validateUsername(username), { + .refine(async (username) => validateUsernameRequest(username), { message: 'Username is already taken.', }), }).refine((data) => data.password === data.confirmPassword, { diff --git a/src/util/createErrorToast.ts b/src/util/createErrorToast.ts new file mode 100644 index 0000000..dce6b60 --- /dev/null +++ b/src/util/createErrorToast.ts @@ -0,0 +1,13 @@ +import toast from 'react-hot-toast'; + +/** + * @param error - The error to display. + * + * Creates a toast message with the error message. + */ +const createErrorToast = (error: unknown) => { + const errorMessage = error instanceof Error ? error.message : 'Something went wrong.'; + toast.error(errorMessage); +}; + +export default createErrorToast;