mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 20:13:49 +00:00
Merge pull request #46 from aaronpo97/create-brewery
Create brewery page
This commit is contained in:
208
package-lock.json
generated
208
package-lock.json
generated
@@ -13,6 +13,8 @@
|
|||||||
"@headlessui/tailwindcss": "^0.1.3",
|
"@headlessui/tailwindcss": "^0.1.3",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
"@mapbox/mapbox-sdk": "^0.15.1",
|
"@mapbox/mapbox-sdk": "^0.15.1",
|
||||||
|
"@mapbox/search-js-core": "^1.0.0-beta.16",
|
||||||
|
"@mapbox/search-js-react": "^1.0.0-beta.16",
|
||||||
"@next/bundle-analyzer": "^13.4.4",
|
"@next/bundle-analyzer": "^13.4.4",
|
||||||
"@prisma/client": "^4.15.0",
|
"@prisma/client": "^4.15.0",
|
||||||
"@react-email/components": "^0.0.6",
|
"@react-email/components": "^0.0.6",
|
||||||
@@ -81,7 +83,7 @@
|
|||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^4.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@@ -638,6 +640,19 @@
|
|||||||
"npm": ">=6.14.13"
|
"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": {
|
"node_modules/@hapi/b64": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
|
||||||
@@ -989,6 +1004,74 @@
|
|||||||
"polyline": "bin/polyline.bin.js"
|
"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": {
|
"node_modules/@mapbox/tiny-sdf": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
||||||
@@ -1973,8 +2056,7 @@
|
|||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
|
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
@@ -2051,8 +2133,7 @@
|
|||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
|
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.3.13",
|
"version": "7.3.13",
|
||||||
@@ -5343,6 +5424,14 @@
|
|||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@@ -8021,6 +8110,11 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/node-addon-api": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
@@ -10568,6 +10662,11 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"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": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
||||||
@@ -11101,16 +11200,16 @@
|
|||||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.1.3",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.17"
|
"node": ">=4.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
@@ -11899,6 +11998,19 @@
|
|||||||
"integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==",
|
"integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==",
|
||||||
"dev": true
|
"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": {
|
"@hapi/b64": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz",
|
||||||
@@ -12178,6 +12290,54 @@
|
|||||||
"meow": "^6.1.1"
|
"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": {
|
"@mapbox/tiny-sdf": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
|
||||||
@@ -12943,8 +13103,7 @@
|
|||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
|
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
@@ -13020,8 +13179,7 @@
|
|||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.3",
|
"version": "0.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz",
|
||||||
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==",
|
"integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/semver": {
|
"@types/semver": {
|
||||||
"version": "7.3.13",
|
"version": "7.3.13",
|
||||||
@@ -15433,6 +15591,14 @@
|
|||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
|
||||||
"dev": true
|
"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": {
|
"for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||||
@@ -17292,6 +17458,11 @@
|
|||||||
"regexparam": "^2.0.1"
|
"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": {
|
"node-addon-api": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||||
@@ -19064,6 +19235,11 @@
|
|||||||
"tslib": "^2.5.0"
|
"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": {
|
"tailwindcss": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz",
|
||||||
@@ -19445,9 +19621,9 @@
|
|||||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "5.1.3",
|
"version": "4.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||||
"integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==",
|
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"unbox-primitive": {
|
"unbox-primitive": {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
"@headlessui/tailwindcss": "^0.1.3",
|
"@headlessui/tailwindcss": "^0.1.3",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
"@mapbox/mapbox-sdk": "^0.15.1",
|
"@mapbox/mapbox-sdk": "^0.15.1",
|
||||||
|
"@mapbox/search-js-core": "^1.0.0-beta.16",
|
||||||
|
"@mapbox/search-js-react": "^1.0.0-beta.16",
|
||||||
"@next/bundle-analyzer": "^13.4.4",
|
"@next/bundle-analyzer": "^13.4.4",
|
||||||
"@prisma/client": "^4.15.0",
|
"@prisma/client": "^4.15.0",
|
||||||
"@react-email/components": "^0.0.6",
|
"@react-email/components": "^0.0.6",
|
||||||
@@ -76,16 +78,16 @@
|
|||||||
"eslint-config-next": "^13.4.4",
|
"eslint-config-next": "^13.4.4",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-react": "^7.32.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"onchange": "^7.1.0",
|
||||||
"postcss": "^8.4.24",
|
"postcss": "^8.4.24",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-jsdoc": "^0.4.2",
|
"prettier-plugin-jsdoc": "^0.4.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||||
"onchange": "^7.1.0",
|
|
||||||
"prisma": "^4.15.0",
|
"prisma": "^4.15.0",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^4.9.0"
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"schema": "./src/prisma/schema.prisma",
|
"schema": "./src/prisma/schema.prisma",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import UserContext from '@/contexts/UserContext';
|
||||||
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
|
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
|
||||||
|
|
||||||
import { Switch } from '@headlessui/react';
|
import { Switch } from '@headlessui/react';
|
||||||
import { useRouter } from 'next/router';
|
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 {
|
interface DeleteAccountProps {
|
||||||
pageState: AccountPageState;
|
pageState: AccountPageState;
|
||||||
@@ -13,6 +16,26 @@ const DeleteAccount: FunctionComponent<DeleteAccountProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const deleteRef = useRef<null | HTMLDialogElement>(null);
|
const deleteRef = useRef<null | HTMLDialogElement>(null);
|
||||||
const router = useRouter();
|
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 (
|
return (
|
||||||
<div className="card w-full space-y-4">
|
<div className="card w-full space-y-4">
|
||||||
@@ -49,10 +72,7 @@ const DeleteAccount: FunctionComponent<DeleteAccountProps> = ({
|
|||||||
<div className="modal-action flex-col space-x-0 space-y-3">
|
<div className="modal-action flex-col space-x-0 space-y-3">
|
||||||
<button
|
<button
|
||||||
className="btn-error btn-sm btn w-full"
|
className="btn-error btn-sm btn w-full"
|
||||||
onClick={async () => {
|
onClick={onDeleteSubmit}
|
||||||
deleteRef.current!.close();
|
|
||||||
await router.replace('/api/users/logout');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Okay, delete my account
|
Okay, delete my account
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -72,14 +72,14 @@ const Security: FunctionComponent<SecurityProps> = ({ dispatch, pageState }) =>
|
|||||||
formValidationSchema={register('password')}
|
formValidationSchema={register('password')}
|
||||||
/>
|
/>
|
||||||
<FormInfo>
|
<FormInfo>
|
||||||
<FormLabel htmlFor="password">Confirm Password</FormLabel>
|
<FormLabel htmlFor="confirm-password">Confirm Password</FormLabel>
|
||||||
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
<FormError>{formState.errors.confirmPassword?.message}</FormError>
|
||||||
</FormInfo>
|
</FormInfo>
|
||||||
<FormTextInput
|
<FormTextInput
|
||||||
type="password"
|
type="password"
|
||||||
disabled={!pageState.securityOpen || formState.isSubmitting}
|
disabled={!pageState.securityOpen || formState.isSubmitting}
|
||||||
error={!!formState.errors.confirmPassword}
|
error={!!formState.errors.confirmPassword}
|
||||||
id="password"
|
id="confirm-password"
|
||||||
formValidationSchema={register('confirmPassword')}
|
formValidationSchema={register('confirmPassword')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -34,15 +34,15 @@ const CreateBeerPostForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
types = [],
|
types = [],
|
||||||
brewery,
|
brewery,
|
||||||
}) => {
|
}) => {
|
||||||
const { register, handleSubmit, formState } = useForm<
|
const {
|
||||||
z.infer<typeof CreateBeerPostWithImagesValidationSchema>
|
register,
|
||||||
>({
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<z.infer<typeof CreateBeerPostWithImagesValidationSchema>>({
|
||||||
resolver: zodResolver(CreateBeerPostWithImagesValidationSchema),
|
resolver: zodResolver(CreateBeerPostWithImagesValidationSchema),
|
||||||
defaultValues: { breweryId: brewery.id },
|
defaultValues: { breweryId: brewery.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { errors, isSubmitting } = formState;
|
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<
|
const onSubmit: SubmitHandler<
|
||||||
z.infer<typeof CreateBeerPostWithImagesValidationSchema>
|
z.infer<typeof CreateBeerPostWithImagesValidationSchema>
|
||||||
> = async (data) => {
|
> = async (data) => {
|
||||||
@@ -51,9 +51,9 @@ const CreateBeerPostForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sendCreateBeerPostRequest(data);
|
const beerPost = await sendCreateBeerPostRequest(data);
|
||||||
await sendUploadBeerImagesRequest({ beerPost: response, images: data.images });
|
await sendUploadBeerImagesRequest({ beerPost, images: data.images });
|
||||||
await router.push(`/beers/${response.id}`);
|
await router.push(`/beers/${beerPost.id}`);
|
||||||
toast.success('Created beer post.');
|
toast.success('Created beer post.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
||||||
|
|||||||
@@ -22,14 +22,19 @@ const toastToClassName = (toastType: Toast['type']) => {
|
|||||||
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
const CustomToast: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toaster position="bottom-right">
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 2500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{(t) => {
|
{(t) => {
|
||||||
const alertType = toastToClassName(t.type);
|
const alertType = toastToClassName(t.type);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`alert ${alertType} flex w-11/12 items-center justify-between shadow-lg animate-in fade-in duration-200 lg:w-4/12`}
|
className={`alert ${alertType} flex w-full items-start justify-between shadow-lg animate-in fade-in duration-200 lg:w-3/12`}
|
||||||
>
|
>
|
||||||
<p className="w-full">{resolveValue(t.message, t)}</p>
|
<p className="w-full text-left">{resolveValue(t.message, t)}</p>
|
||||||
{t.type !== 'loading' && (
|
{t.type !== 'loading' && (
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const FormPageLayout: FC<FormPageLayoutProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="my-20 flex flex-col items-center justify-center">
|
<div className="my-20 flex flex-col items-center justify-center">
|
||||||
<div className="w-10/12 lg:w-8/12 2xl:w-6/12">
|
<div className="w-11/12 lg:w-9/12 2xl:w-7/12">
|
||||||
<div className="tooltip tooltip-right" data-tip={backLinkText}>
|
<div className="tooltip tooltip-right" data-tip={backLinkText}>
|
||||||
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
<Link href={backLink} className="btn-ghost btn-sm btn p-0">
|
||||||
<BiArrowBack className="text-xl" />
|
<BiArrowBack className="text-xl" />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface FormInputProps {
|
|||||||
id: string;
|
id: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
autoComplete?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +34,7 @@ interface FormInputProps {
|
|||||||
* @param param0.id The id of the input.
|
* @param param0.id The id of the input.
|
||||||
* @param param0.height The height of the input.
|
* @param param0.height The height of the input.
|
||||||
* @param param0.disabled Whether or not the input is disabled.
|
* @param param0.disabled Whether or not the input is disabled.
|
||||||
|
* @param param0.autoComplete The autocomplete value for the input.
|
||||||
*/
|
*/
|
||||||
const FormTextInput: FunctionComponent<FormInputProps> = ({
|
const FormTextInput: FunctionComponent<FormInputProps> = ({
|
||||||
placeholder = '',
|
placeholder = '',
|
||||||
|
|||||||
@@ -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<typeof GetUserSchema>;
|
|
||||||
error?: unknown;
|
|
||||||
isLoading: boolean;
|
|
||||||
mutate?: ReturnType<typeof useUser>['mutate'];
|
|
||||||
}>({ isLoading: true });
|
|
||||||
|
|
||||||
export default UserContext;
|
|
||||||
24
src/contexts/UserContext.tsx
Normal file
24
src/contexts/UserContext.tsx
Normal file
@@ -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<typeof GetUserSchema>;
|
||||||
|
error?: unknown;
|
||||||
|
isLoading: boolean;
|
||||||
|
mutate?: ReturnType<typeof useUser>['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 (
|
||||||
|
<UserContext.Provider value={{ isLoading, error, mutate, user }}>
|
||||||
|
{children}
|
||||||
|
</UserContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -46,7 +46,12 @@ const useUser = () => {
|
|||||||
return parsedPayload.data;
|
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;
|
export default useUser;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import UserContext from '@/contexts/UserContext';
|
import UserContext, { AuthProvider } from '@/contexts/UserContext';
|
||||||
|
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
@@ -13,15 +13,12 @@ import Layout from '@/components/ui/Layout';
|
|||||||
import useUser from '@/hooks/auth/useUser';
|
import useUser from '@/hooks/auth/useUser';
|
||||||
import CustomToast from '@/components/ui/CustomToast';
|
import CustomToast from '@/components/ui/CustomToast';
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
const spaceGrotesk = Space_Grotesk({ subsets: ['latin'] });
|
||||||
subsets: ['latin'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
themeChange(false);
|
themeChange(false);
|
||||||
}, []);
|
}, []);
|
||||||
const { user, isLoading, error, mutate } = useUser();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -38,15 +35,17 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0"
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<UserContext.Provider value={{ user, isLoading, error, mutate }}>
|
<AuthProvider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<CustomToast>
|
<CustomToast>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</CustomToast>
|
</CustomToast>
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserContext.Provider>
|
</AuthProvider>
|
||||||
|
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { NextApiResponse } from 'next';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import processImageDataIntoDB from '@/services/BeerImage/processImageDataIntoDB';
|
import addBeerImageToDB from '@/services/BeerImage/addBeerImageToDB';
|
||||||
import ImageMetadataValidationSchema from '@/services/types/ImageSchema/ImageMetadataValidationSchema';
|
import ImageMetadataValidationSchema from '@/services/types/ImageSchema/ImageMetadataValidationSchema';
|
||||||
|
|
||||||
const { storage } = cloudinaryConfig;
|
const { storage } = cloudinaryConfig;
|
||||||
@@ -50,7 +50,7 @@ const processImageData = async (
|
|||||||
throw new ServerError('No images uploaded', 400);
|
throw new ServerError('No images uploaded', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const beerImages = await processImageDataIntoDB({
|
const beerImages = await addBeerImageToDB({
|
||||||
alt: body.alt,
|
alt: body.alt,
|
||||||
caption: body.caption,
|
caption: body.caption,
|
||||||
beerPostId: req.query.id,
|
beerPostId: req.query.id,
|
||||||
|
|||||||
86
src/pages/api/breweries/[id]/images/index.ts
Normal file
86
src/pages/api/breweries/[id]/images/index.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import { createRouter, expressWrapper } from 'next-connect';
|
||||||
|
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
|
||||||
|
import multer from 'multer';
|
||||||
|
|
||||||
|
import cloudinaryConfig from '@/config/cloudinary';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import ImageMetadataValidationSchema from '@/services/types/ImageSchema/ImageMetadataValidationSchema';
|
||||||
|
import addBreweryImageToDB from '@/services/BreweryImage/addBreweryImageToDB';
|
||||||
|
|
||||||
|
const { storage } = cloudinaryConfig;
|
||||||
|
|
||||||
|
const fileFilter: multer.Options['fileFilter'] = (req, file, cb) => {
|
||||||
|
const { mimetype } = file;
|
||||||
|
|
||||||
|
const isImage = mimetype.startsWith('image/');
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
cb(null, false);
|
||||||
|
}
|
||||||
|
cb(null, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadMiddleware = expressWrapper(
|
||||||
|
multer({ storage, fileFilter, limits: { files: 5, fileSize: 15 * 1024 * 1024 } }).array(
|
||||||
|
'images',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
interface UploadBreweryPostImagesRequest extends UserExtendedNextApiRequest {
|
||||||
|
files?: Express.Multer.File[];
|
||||||
|
query: { id: string };
|
||||||
|
body: z.infer<typeof ImageMetadataValidationSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processImageData = async (
|
||||||
|
req: UploadBreweryPostImagesRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { files, user, body } = req;
|
||||||
|
|
||||||
|
if (!files || !files.length) {
|
||||||
|
throw new ServerError('No images uploaded', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const breweryImages = await addBreweryImageToDB({
|
||||||
|
alt: body.alt,
|
||||||
|
caption: body.caption,
|
||||||
|
breweryPostId: req.query.id,
|
||||||
|
userId: user!.id,
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: `Successfully uploaded ${breweryImages.length} image${
|
||||||
|
breweryImages.length > 1 ? 's' : ''
|
||||||
|
}`,
|
||||||
|
statusCode: 200,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
UploadBreweryPostImagesRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
getCurrentUser,
|
||||||
|
// @ts-expect-error
|
||||||
|
uploadMiddleware,
|
||||||
|
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||||
|
processImageData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
|
|
||||||
|
export const config = { api: { bodyParser: false } };
|
||||||
76
src/pages/api/breweries/create.ts
Normal file
76
src/pages/api/breweries/create.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||||
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
|
import { createRouter } from 'next-connect';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
|
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
import CreateBreweryPostSchema from '@/services/BreweryPost/types/CreateBreweryPostSchema';
|
||||||
|
import createNewBreweryPost from '@/services/BreweryPost/createNewBreweryPost';
|
||||||
|
import geocode from '@/config/mapbox/geocoder';
|
||||||
|
import ServerError from '@/config/util/ServerError';
|
||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
|
||||||
|
interface CreateBreweryPostRequest extends UserExtendedNextApiRequest {
|
||||||
|
body: z.infer<typeof CreateBreweryPostSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBreweryPost = async (
|
||||||
|
req: CreateBreweryPostRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
const { name, description, dateEstablished, address, city, country, region } = req.body;
|
||||||
|
const userId = req.user!.id;
|
||||||
|
|
||||||
|
const fullAddress = `${address}, ${city}, ${region}, ${country}`;
|
||||||
|
|
||||||
|
const geocoded = await geocode(fullAddress);
|
||||||
|
|
||||||
|
if (!geocoded) {
|
||||||
|
throw new ServerError('Address is not valid', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [latitude, longitude] = geocoded.center;
|
||||||
|
|
||||||
|
const location = await DBClient.instance.location.create({
|
||||||
|
data: {
|
||||||
|
address,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
stateOrProvince: region,
|
||||||
|
coordinates: [latitude, longitude],
|
||||||
|
postedBy: { connect: { id: userId } },
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const newBreweryPost = await createNewBreweryPost({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
locationId: location.id,
|
||||||
|
dateEstablished,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Brewery post created successfully',
|
||||||
|
statusCode: 201,
|
||||||
|
payload: newBreweryPost,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter<
|
||||||
|
CreateBreweryPostRequest,
|
||||||
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
validateRequest({ bodySchema: CreateBreweryPostSchema }),
|
||||||
|
getCurrentUser,
|
||||||
|
createBreweryPost,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
export default handler;
|
||||||
@@ -4,6 +4,7 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
|||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import ServerError from '@/config/util/ServerError';
|
import ServerError from '@/config/util/ServerError';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import deleteUserById from '@/services/User/deleteUserById';
|
||||||
import findUserByEmail from '@/services/User/findUserByEmail';
|
import findUserByEmail from '@/services/User/findUserByEmail';
|
||||||
import findUserById from '@/services/User/findUserById';
|
import findUserById from '@/services/User/findUserById';
|
||||||
import findUserByUsername from '@/services/User/findUserByUsername';
|
import findUserByUsername from '@/services/User/findUserByUsername';
|
||||||
@@ -21,11 +22,12 @@ const EditUserSchema = BaseCreateUserSchema.pick({
|
|||||||
lastName: true,
|
lastName: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface EditUserRequest extends UserExtendedNextApiRequest {
|
interface UserRouteRequest extends UserExtendedNextApiRequest {
|
||||||
|
query: { id: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditUserRequest extends UserRouteRequest {
|
||||||
body: z.infer<typeof EditUserSchema>;
|
body: z.infer<typeof EditUserSchema>;
|
||||||
query: {
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIfUserCanEditUser = async (
|
const checkIfUserCanEditUser = async (
|
||||||
@@ -41,7 +43,7 @@ const checkIfUserCanEditUser = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authenticatedUser.id !== userToUpdate.id) {
|
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();
|
await next();
|
||||||
@@ -88,6 +90,24 @@ const editUser = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteUser = async (
|
||||||
|
req: UserRouteRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||||
|
) => {
|
||||||
|
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<
|
const router = createRouter<
|
||||||
EditUserRequest,
|
EditUserRequest,
|
||||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||||
@@ -103,6 +123,15 @@ router.put(
|
|||||||
editUser,
|
editUser,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
getCurrentUser,
|
||||||
|
validateRequest({
|
||||||
|
querySchema: z.object({ id: z.string().cuid() }),
|
||||||
|
}),
|
||||||
|
checkIfUserCanEditUser,
|
||||||
|
deleteUser,
|
||||||
|
);
|
||||||
|
|
||||||
const handler = router.handler(NextConnectOptions);
|
const handler = router.handler(NextConnectOptions);
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
@@ -5,7 +5,6 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
|||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import DBClient from '@/prisma/DBClient';
|
import DBClient from '@/prisma/DBClient';
|
||||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { createRouter } from 'next-connect';
|
import { createRouter } from 'next-connect';
|
||||||
@@ -23,26 +22,15 @@ const updatePassword = async (
|
|||||||
const hash = await hashPassword(password);
|
const hash = await hashPassword(password);
|
||||||
|
|
||||||
const user = req.user!;
|
const user = req.user!;
|
||||||
const updatedUser: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
|
await DBClient.instance.user.update({
|
||||||
data: { hash },
|
data: { hash },
|
||||||
where: { id: user.id },
|
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({
|
res.json({
|
||||||
message: 'Updated user password.',
|
message: 'Updated user password.',
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
success: true,
|
success: true,
|
||||||
payload: updatedUser,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const router = createRouter<
|
const router = createRouter<
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import findUserByEmail from '@/services/User/findUserByEmail';
|
|||||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
|
||||||
|
import { NODE_ENV } from '@/config/env';
|
||||||
import sendConfirmationEmail from '@/services/User/sendConfirmationEmail';
|
import sendConfirmationEmail from '@/services/User/sendConfirmationEmail';
|
||||||
|
|
||||||
interface RegisterUserRequest extends NextApiRequest {
|
interface RegisterUserRequest extends NextApiRequest {
|
||||||
@@ -44,7 +45,9 @@ const registerUser = async (req: RegisterUserRequest, res: NextApiResponse) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendConfirmationEmail(user);
|
if (NODE_ENV === 'production') {
|
||||||
|
await sendConfirmationEmail(user);
|
||||||
|
}
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export const getServerSideProps = withPageAuthRequired<CreateBeerPageProps>(
|
|||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
const breweryPost = await getBreweryPostById(id);
|
const breweryPost = await getBreweryPostById(id);
|
||||||
|
|
||||||
const beerTypes = await DBClient.instance.beerType.findMany();
|
const beerTypes = await DBClient.instance.beerType.findMany();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
279
src/pages/breweries/create.tsx
Normal file
279
src/pages/breweries/create.tsx
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
import Button from '@/components/ui/forms/Button';
|
||||||
|
import FormError from '@/components/ui/forms/FormError';
|
||||||
|
import FormInfo from '@/components/ui/forms/FormInfo';
|
||||||
|
import FormLabel from '@/components/ui/forms/FormLabel';
|
||||||
|
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
|
||||||
|
import FormSegment from '@/components/ui/forms/FormSegment';
|
||||||
|
import FormTextArea from '@/components/ui/forms/FormTextArea';
|
||||||
|
import FormTextInput from '@/components/ui/forms/FormTextInput';
|
||||||
|
|
||||||
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
|
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
||||||
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { AddressAutofillRetrieveResponse } from '@mapbox/search-js-core';
|
||||||
|
import { GetServerSideProps, NextPage } from 'next';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { FieldError, SubmitHandler, useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { FaBeer } from 'react-icons/fa';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import CreateBreweryPostSchema from '@/services/BreweryPost/types/CreateBreweryPostSchema';
|
||||||
|
import UploadImageValidationSchema from '@/services/types/ImageSchema/UploadImageValidationSchema';
|
||||||
|
import sendUploadBreweryImagesRequest from '@/requests/BreweryImage/sendUploadBreweryImageRequest';
|
||||||
|
|
||||||
|
const AddressAutofill = dynamic(
|
||||||
|
() => import('@mapbox/search-js-react').then((mod) => mod.AddressAutofill),
|
||||||
|
{ ssr: false },
|
||||||
|
);
|
||||||
|
const CreateBreweryPostWithImagesSchema = CreateBreweryPostSchema.merge(
|
||||||
|
UploadImageValidationSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendCreateBreweryPostRequest = async (
|
||||||
|
data: z.infer<typeof CreateBreweryPostSchema>,
|
||||||
|
) => {
|
||||||
|
const response = await fetch('/api/breweries/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 parsing failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedPayload = BreweryPostQueryResult.safeParse(parsed.data.payload);
|
||||||
|
if (!parsedPayload.success) {
|
||||||
|
throw new Error('API response payload parsing failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedPayload.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateBreweryPage: NextPage = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = useForm<z.infer<typeof CreateBreweryPostWithImagesSchema>>({
|
||||||
|
resolver: zodResolver(CreateBreweryPostWithImagesSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<
|
||||||
|
z.infer<typeof CreateBreweryPostWithImagesSchema>
|
||||||
|
> = async (data) => {
|
||||||
|
const loadingToast = toast.loading('Creating brewery...');
|
||||||
|
try {
|
||||||
|
if (!(data.images instanceof FileList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const breweryPost = await sendCreateBreweryPostRequest(data);
|
||||||
|
await sendUploadBreweryImagesRequest({ breweryPost, images: data.images });
|
||||||
|
await router.push(`/breweries/${breweryPost.id}`);
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
toast.success('Created brewery.');
|
||||||
|
} catch (error) {
|
||||||
|
toast.remove(loadingToast);
|
||||||
|
reset();
|
||||||
|
createErrorToast(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAutoCompleteChange = (address: string) => {
|
||||||
|
setValue('address', address);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAutoCompleteRetrieve = (address: AddressAutofillRetrieveResponse) => {
|
||||||
|
const { country, region, place } = address.features[0].properties as unknown as {
|
||||||
|
country?: string;
|
||||||
|
region?: string;
|
||||||
|
place?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
setValue('country', country);
|
||||||
|
setValue('region', region);
|
||||||
|
setValue('city', place!);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Create Brewery</title>
|
||||||
|
</Head>
|
||||||
|
<div className="flex w-full flex-col items-center justify-center">
|
||||||
|
<div className="w-full">
|
||||||
|
<FormPageLayout
|
||||||
|
backLink="/breweries"
|
||||||
|
backLinkText="Back to Breweries"
|
||||||
|
headingText="Create Brewery"
|
||||||
|
headingIcon={FaBeer}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="form-control"
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="name">Name</FormLabel>
|
||||||
|
<FormError>{errors.name?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
placeholder="Lorem Ipsum Brewing Company"
|
||||||
|
formValidationSchema={register('name')}
|
||||||
|
error={!!errors.name}
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="description">Description</FormLabel>
|
||||||
|
<FormError>{errors.description?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextArea
|
||||||
|
placeholder="We make beer, and we make it good."
|
||||||
|
formValidationSchema={register('description')}
|
||||||
|
error={!!errors.description}
|
||||||
|
rows={4}
|
||||||
|
id="description"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="dateEstablished">Date Established</FormLabel>
|
||||||
|
<FormError>{errors.dateEstablished?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
placeholder="2021-01-01"
|
||||||
|
formValidationSchema={register('dateEstablished')}
|
||||||
|
error={!!errors.dateEstablished}
|
||||||
|
type="date"
|
||||||
|
id="dateEstablished"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="images">Images</FormLabel>
|
||||||
|
<FormError>
|
||||||
|
{(errors.images as FieldError | undefined)?.message}
|
||||||
|
</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
{...register('images')}
|
||||||
|
multiple
|
||||||
|
className="file-input-bordered file-input w-full"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="address">Address</FormLabel>
|
||||||
|
<FormError>{errors.address?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<AddressAutofill
|
||||||
|
accessToken={process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN!}
|
||||||
|
onRetrieve={onAutoCompleteRetrieve}
|
||||||
|
onChange={onAutoCompleteChange}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="address"
|
||||||
|
type="text"
|
||||||
|
placeholder="1234 Main St"
|
||||||
|
className={`input-bordered input w-full appearance-none rounded-lg transition ease-in-out ${
|
||||||
|
errors.address?.message ? 'input-error' : ''
|
||||||
|
}`}
|
||||||
|
{...register('address')}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</AddressAutofill>
|
||||||
|
</FormSegment>
|
||||||
|
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="city">City</FormLabel>
|
||||||
|
<FormError>{errors.city?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
placeholder="Toronto"
|
||||||
|
formValidationSchema={register('city')}
|
||||||
|
error={!!errors.city}
|
||||||
|
type="text"
|
||||||
|
id="city"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/2">
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="region">Region</FormLabel>
|
||||||
|
<FormError>{errors.region?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
placeholder="Ontario"
|
||||||
|
formValidationSchema={register('region')}
|
||||||
|
error={!!errors.region}
|
||||||
|
type="text"
|
||||||
|
id="region"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormInfo>
|
||||||
|
<FormLabel htmlFor="country">Country</FormLabel>
|
||||||
|
<FormError>{errors.country?.message}</FormError>
|
||||||
|
</FormInfo>
|
||||||
|
<FormSegment>
|
||||||
|
<FormTextInput
|
||||||
|
placeholder="Canada"
|
||||||
|
formValidationSchema={register('country')}
|
||||||
|
error={!!errors.country}
|
||||||
|
type="text"
|
||||||
|
id="country"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</FormSegment>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button type="submit" isSubmitting={isSubmitting}>
|
||||||
|
Create Brewery Post
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormPageLayout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateBreweryPage;
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = withPageAuthRequired();
|
||||||
34
src/requests/BreweryImage/sendUploadBreweryImageRequest.ts
Normal file
34
src/requests/BreweryImage/sendUploadBreweryImageRequest.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import BreweryPostQueryResult from '@/services/BreweryPost/types/BreweryPostQueryResult';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
interface SendUploadBeerImagesRequestArgs {
|
||||||
|
breweryPost: z.infer<typeof BreweryPostQueryResult>;
|
||||||
|
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;
|
||||||
@@ -13,7 +13,7 @@ interface SendEditUserRequestArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendEditUserRequest = async ({ user, data }: 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),
|
body: JSON.stringify(data),
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
import { UpdatePasswordSchema } from '@/services/User/schema/CreateUserValidationSchemas';
|
||||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@@ -21,13 +20,7 @@ const sendUpdatePasswordRequest = async (data: z.infer<typeof UpdatePasswordSche
|
|||||||
throw new Error('API response validation failed.');
|
throw new Error('API response validation failed.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
|
return parsed.data;
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
|
||||||
throw new Error('API payload validation failed.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedPayload.data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default sendUpdatePasswordRequest;
|
export default sendUpdatePasswordRequest;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ interface ProcessImageDataArgs {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processImageDataIntoDB = ({
|
const addBeerImageToDB = ({
|
||||||
alt,
|
alt,
|
||||||
caption,
|
caption,
|
||||||
files,
|
files,
|
||||||
@@ -36,4 +36,4 @@ const processImageDataIntoDB = ({
|
|||||||
return Promise.all(beerImagePromises);
|
return Promise.all(beerImagePromises);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default processImageDataIntoDB;
|
export default addBeerImageToDB;
|
||||||
39
src/services/BreweryImage/addBreweryImageToDB.ts
Normal file
39
src/services/BreweryImage/addBreweryImageToDB.ts
Normal file
@@ -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<typeof ImageMetadataValidationSchema>['alt'];
|
||||||
|
caption: z.infer<typeof ImageMetadataValidationSchema>['caption'];
|
||||||
|
breweryPostId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBreweryImageToDB = ({
|
||||||
|
alt,
|
||||||
|
caption,
|
||||||
|
files,
|
||||||
|
breweryPostId,
|
||||||
|
userId,
|
||||||
|
}: ProcessImageDataArgs) => {
|
||||||
|
const breweryImagePromises: Promise<BreweryImage>[] = [];
|
||||||
|
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;
|
||||||
55
src/services/BreweryPost/createNewBreweryPost.ts
Normal file
55
src/services/BreweryPost/createNewBreweryPost.ts
Normal file
@@ -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<typeof CreateNewBreweryPostWithUserAndLocationSchema>) => {
|
||||||
|
const breweryPost: z.infer<typeof BreweryPostQueryResult> =
|
||||||
|
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;
|
||||||
55
src/services/BreweryPost/types/CreateBreweryPostSchema.ts
Normal file
55
src/services/BreweryPost/types/CreateBreweryPostSchema.ts
Normal file
@@ -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;
|
||||||
25
src/services/User/deleteUserById.ts
Normal file
25
src/services/User/deleteUserById.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import DBClient from '@/prisma/DBClient';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import GetUserSchema from './schema/GetUserSchema';
|
||||||
|
|
||||||
|
const deleteUserById = async (id: string) => {
|
||||||
|
const deletedUser: z.infer<typeof GetUserSchema> | 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;
|
||||||
@@ -37,6 +37,9 @@ export const BaseCreateUserSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
dateOfBirth: z
|
dateOfBirth: z
|
||||||
.string()
|
.string()
|
||||||
|
.refine((val) => !Number.isNaN(Date.parse(val)), {
|
||||||
|
message: 'Date is invalid.',
|
||||||
|
})
|
||||||
.refine((dateOfBirth) => new Date(dateOfBirth) <= MINIMUM_DATE_OF_BIRTH, {
|
.refine((dateOfBirth) => new Date(dateOfBirth) <= MINIMUM_DATE_OF_BIRTH, {
|
||||||
message: 'You must be at least 19 years old to register.',
|
message: 'You must be at least 19 years old to register.',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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.
|
* @template P - A generic type that represents an object with string keys and any values.
|
||||||
* It defaults to an empty object.
|
* It defaults to an empty object.
|
||||||
* @template Q - A generic type that represents a parsed URL query object. It defaults to
|
* @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
|
* @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
|
* @param context - The context object containing information about the incoming HTTP
|
||||||
* request.
|
* request.
|
||||||
* @param session - An awaited value of the return type of the getLoginSession function.
|
* @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.
|
* @returns A promise that resolves to the result of the server-side rendering process.
|
||||||
*/
|
*/
|
||||||
export type ExtendedGetServerSideProps<
|
export type ExtendedGetServerSideProps<
|
||||||
P extends { [key: string]: any } = { [key: string]: any },
|
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
|
* A Higher Order Function that adds an authentication requirement to a Next.js
|
||||||
* server-side page component.
|
* server-side page component.
|
||||||
*
|
*
|
||||||
* @param fn An async function that receives the GetServerSidePropsContext and
|
* @param fn An async function that receives the `GetServerSidePropsContext` and
|
||||||
* authenticated session as arguments and returns a GetServerSidePropsResult with props
|
* authenticated session as arguments and returns a `GetServerSidePropsResult` with
|
||||||
* for the wrapped component.
|
* props for the wrapped component.
|
||||||
* @returns A promise that resolves to a GetServerSidePropsResult object with props for
|
* @returns A promise that resolves to a `GetServerSidePropsResult` object with props for
|
||||||
* the wrapped component. If authentication is successful, the GetServerSidePropsResult
|
* the wrapped component.
|
||||||
* will include props generated by the wrapped component's getServerSideProps method. If
|
*
|
||||||
* authentication fails, the GetServerSidePropsResult will include a redirect to the
|
* If authentication is successful, the `GetServerSidePropsResult` will include props
|
||||||
* login page.
|
* generated by the wrapped component's `getServerSideProps` method. If authentication
|
||||||
|
* fails, the `GetServerSidePropsResult` will include a redirect to the login page.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const withPageAuthRequired =
|
const withPageAuthRequired =
|
||||||
|
|||||||
Reference in New Issue
Block a user