mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 02:39:03 +00:00
Merge pull request #67 from aaronpo97/dev
feat: implement user follow feature, remove multer-storage-cloudinary
This commit is contained in:
@@ -122,7 +122,7 @@ npm install
|
||||
|
||||
```bash
|
||||
echo "BASE_URL=
|
||||
CLOUDINARY_CLOUD_NAME=
|
||||
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
|
||||
CLOUDINARY_KEY=
|
||||
CLOUDINARY_SECRET=
|
||||
CONFIRMATION_TOKEN_SECRET=
|
||||
@@ -149,8 +149,8 @@ SPARKPOST_SENDER_ADDRESS=" > .env
|
||||
- `BASE_URL` is the base URL of the application.
|
||||
- For example, if you are running the application locally, you can set this to
|
||||
`http://localhost:3000`.
|
||||
- `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the credentials
|
||||
for your Cloudinary account.
|
||||
- `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the
|
||||
credentials for your Cloudinary account.
|
||||
- You can create a free account [here](https://cloudinary.com/users/register/free).
|
||||
- `CONFIRMATION_TOKEN_SECRET` is the secret used to sign the confirmation token used for
|
||||
email confirmation.
|
||||
|
||||
147
package-lock.json
generated
147
package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"@react-email/components": "^0.0.11",
|
||||
"@react-email/render": "^0.0.9",
|
||||
"@react-email/tailwind": "^0.0.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@vercel/analytics": "^1.1.0",
|
||||
"argon2": "^0.31.1",
|
||||
"cloudinary": "^1.41.0",
|
||||
@@ -30,8 +31,8 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mapbox-gl": "^2.15.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-storage-cloudinary": "^4.0.0",
|
||||
"next": "^14.0.3",
|
||||
"next-cloudinary": "^5.10.0",
|
||||
"next-connect": "^1.0.0-next.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -289,6 +290,36 @@
|
||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@cloudinary-util/url-loader": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary-util/url-loader/-/url-loader-3.16.0.tgz",
|
||||
"integrity": "sha512-KqZYuSkQg5FnlREecay5sJTXYdfg/3PaS9Az+XZbCcrdbQQJtWotyE4K3S3F7YBSqTdBItE/XqmgzmmcGLZ3Pw==",
|
||||
"dependencies": {
|
||||
"@cloudinary-util/util": "2.3.0",
|
||||
"@cloudinary/url-gen": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudinary-util/util": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary-util/util/-/util-2.3.0.tgz",
|
||||
"integrity": "sha512-0Gojd+ZRQjJQmlBEAa8Ua94amvx7uWHoUzVUEGi1S8bgF1wPcMqG07cSUGQfRwHsFQ/9XOesx76Df622E+CevA=="
|
||||
},
|
||||
"node_modules/@cloudinary/transformation-builder-sdk": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary/transformation-builder-sdk/-/transformation-builder-sdk-1.8.0.tgz",
|
||||
"integrity": "sha512-/QLSDDI+rfYH3bFH+DpgEk8NgSoeqwiea9GW5VXplcpebQOOcFqOzNFM6Hlik3PDqq4JMP4s3E7BoPZ1Rdk/IQ==",
|
||||
"dependencies": {
|
||||
"@cloudinary/url-gen": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudinary/url-gen": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary/url-gen/-/url-gen-1.13.0.tgz",
|
||||
"integrity": "sha512-p7ASCX2fZw1JWtMG2ADd/Y0TfvE5SX9v0TDJDlzXX3OlNxLSuqzLWGgj5oJD4ZTo62FdxH24N8HPZ7NXEXtj5g==",
|
||||
"dependencies": {
|
||||
"@cloudinary/transformation-builder-sdk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -2030,7 +2061,6 @@
|
||||
"version": "1.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
||||
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -2057,7 +2087,6 @@
|
||||
"version": "3.4.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
||||
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -2078,10 +2107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz",
|
||||
"integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==",
|
||||
"dev": true,
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -2093,7 +2121,6 @@
|
||||
"version": "4.17.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
|
||||
"integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
@@ -2114,8 +2141,7 @@
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.13",
|
||||
@@ -2190,8 +2216,7 @@
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg=="
|
||||
},
|
||||
"node_modules/@types/minimist": {
|
||||
"version": "1.2.3",
|
||||
@@ -2285,14 +2310,12 @@
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg=="
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.25",
|
||||
@@ -2370,7 +2393,6 @@
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
||||
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
@@ -2380,7 +2402,6 @@
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
||||
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/mime": "*",
|
||||
@@ -7474,14 +7495,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer-storage-cloudinary": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz",
|
||||
"integrity": "sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA==",
|
||||
"peerDependencies": {
|
||||
"cloudinary": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
@@ -7565,6 +7578,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-cloudinary": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/next-cloudinary/-/next-cloudinary-5.10.0.tgz",
|
||||
"integrity": "sha512-FmLvteYJjpvE69pNZlp/xzwf32/iNuYM9INrh0GTNhgKxoKP/Ej/ij+kjKkZq8eAA8epSeDh3IUoewwRpJfuTQ==",
|
||||
"dependencies": {
|
||||
"@cloudinary-util/url-loader": "^3.16.0",
|
||||
"@cloudinary-util/util": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12 || ^13 || ^14",
|
||||
"react": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/next-connect": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz",
|
||||
@@ -10941,6 +10967,36 @@
|
||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
||||
"dev": true
|
||||
},
|
||||
"@cloudinary-util/url-loader": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary-util/url-loader/-/url-loader-3.16.0.tgz",
|
||||
"integrity": "sha512-KqZYuSkQg5FnlREecay5sJTXYdfg/3PaS9Az+XZbCcrdbQQJtWotyE4K3S3F7YBSqTdBItE/XqmgzmmcGLZ3Pw==",
|
||||
"requires": {
|
||||
"@cloudinary-util/util": "2.3.0",
|
||||
"@cloudinary/url-gen": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"@cloudinary-util/util": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary-util/util/-/util-2.3.0.tgz",
|
||||
"integrity": "sha512-0Gojd+ZRQjJQmlBEAa8Ua94amvx7uWHoUzVUEGi1S8bgF1wPcMqG07cSUGQfRwHsFQ/9XOesx76Df622E+CevA=="
|
||||
},
|
||||
"@cloudinary/transformation-builder-sdk": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary/transformation-builder-sdk/-/transformation-builder-sdk-1.8.0.tgz",
|
||||
"integrity": "sha512-/QLSDDI+rfYH3bFH+DpgEk8NgSoeqwiea9GW5VXplcpebQOOcFqOzNFM6Hlik3PDqq4JMP4s3E7BoPZ1Rdk/IQ==",
|
||||
"requires": {
|
||||
"@cloudinary/url-gen": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"@cloudinary/url-gen": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudinary/url-gen/-/url-gen-1.13.0.tgz",
|
||||
"integrity": "sha512-p7ASCX2fZw1JWtMG2ADd/Y0TfvE5SX9v0TDJDlzXX3OlNxLSuqzLWGgj5oJD4ZTo62FdxH24N8HPZ7NXEXtj5g==",
|
||||
"requires": {
|
||||
"@cloudinary/transformation-builder-sdk": "^1.7.0"
|
||||
}
|
||||
},
|
||||
"@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@@ -12079,7 +12135,6 @@
|
||||
"version": "1.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
||||
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -12106,7 +12161,6 @@
|
||||
"version": "3.4.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
||||
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -12127,10 +12181,9 @@
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz",
|
||||
"integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==",
|
||||
"dev": true,
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"requires": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -12142,7 +12195,6 @@
|
||||
"version": "4.17.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
|
||||
"integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
@@ -12163,8 +12215,7 @@
|
||||
"@types/http-errors": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
||||
},
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.13",
|
||||
@@ -12239,8 +12290,7 @@
|
||||
"@types/mime": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg=="
|
||||
},
|
||||
"@types/minimist": {
|
||||
"version": "1.2.3",
|
||||
@@ -12333,14 +12383,12 @@
|
||||
"@types/qs": {
|
||||
"version": "6.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg=="
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "18.2.25",
|
||||
@@ -12417,7 +12465,6 @@
|
||||
"version": "0.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
||||
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
@@ -12427,7 +12474,6 @@
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
||||
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/mime": "*",
|
||||
@@ -16062,12 +16108,6 @@
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"multer-storage-cloudinary": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz",
|
||||
"integrity": "sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA==",
|
||||
"requires": {}
|
||||
},
|
||||
"murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
@@ -16117,6 +16157,15 @@
|
||||
"watchpack": "2.4.0"
|
||||
}
|
||||
},
|
||||
"next-cloudinary": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/next-cloudinary/-/next-cloudinary-5.10.0.tgz",
|
||||
"integrity": "sha512-FmLvteYJjpvE69pNZlp/xzwf32/iNuYM9INrh0GTNhgKxoKP/Ej/ij+kjKkZq8eAA8epSeDh3IUoewwRpJfuTQ==",
|
||||
"requires": {
|
||||
"@cloudinary-util/url-loader": "^3.16.0",
|
||||
"@cloudinary-util/util": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"next-connect": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz",
|
||||
|
||||
11
package.json
11
package.json
@@ -25,6 +25,7 @@
|
||||
"@react-email/components": "^0.0.11",
|
||||
"@react-email/render": "^0.0.9",
|
||||
"@react-email/tailwind": "^0.0.12",
|
||||
"@types/express": "^4.17.21",
|
||||
"@vercel/analytics": "^1.1.0",
|
||||
"argon2": "^0.31.1",
|
||||
"cloudinary": "^1.41.0",
|
||||
@@ -35,8 +36,8 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mapbox-gl": "^2.15.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-storage-cloudinary": "^4.0.0",
|
||||
"next": "^14.0.3",
|
||||
"next-cloudinary": "^5.10.0",
|
||||
"next-connect": "^1.0.0-next.3",
|
||||
"passport": "^0.6.0",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -64,28 +65,28 @@
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.2",
|
||||
"@types/passport-local": "^1.0.35",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/sparkpost": "^2.1.5",
|
||||
"@vercel/fetch": "^7.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"daisyui": "^3.9.2",
|
||||
"dotenv-cli": "^7.2.1",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-next": "^13.5.4",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint": "^8.51.0",
|
||||
"generate-password": "^1.7.1",
|
||||
"onchange": "^7.1.0",
|
||||
"postcss": "^8.4.26",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-jsdoc": "^1.0.2",
|
||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.6.0",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
import { z } from 'zod';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import { FaUser } from 'react-icons/fa';
|
||||
@@ -25,11 +25,12 @@ const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
<CldImage
|
||||
src={userAvatar.path}
|
||||
alt="user avatar"
|
||||
width={1000}
|
||||
height={1000}
|
||||
crop="fill"
|
||||
className="h-full w-full object-cover mask mask-circle"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import Link from 'next/link';
|
||||
import { FC, useContext } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
import { z } from 'zod';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
import BeerPostLikeButton from '../BeerById/BeerPostLikeButton';
|
||||
|
||||
const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) => {
|
||||
@@ -16,9 +17,10 @@ const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) =
|
||||
<figure className="h-96">
|
||||
<Link href={`/beers/${post.id}`} className="h-full object-cover">
|
||||
{post.beerImages.length > 0 && (
|
||||
<Image
|
||||
<CldImage
|
||||
src={post.beerImages[0].path}
|
||||
alt={post.name}
|
||||
crop="fill"
|
||||
width="3000"
|
||||
height="3000"
|
||||
className="h-full object-cover"
|
||||
|
||||
@@ -4,7 +4,8 @@ import BreweryPostQueryResult from '@/services/BreweryPost/schema/BreweryPostQue
|
||||
import { FC, useContext } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { z } from 'zod';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
import BreweryPostLikeButton from './BreweryPostLikeButton';
|
||||
|
||||
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
||||
@@ -17,11 +18,12 @@ const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
||||
<figure className="card-image h-96">
|
||||
<Link href={`/breweries/${brewery.id}`} className="h-full object-cover">
|
||||
{brewery.breweryImages.length > 0 && (
|
||||
<Image
|
||||
<CldImage
|
||||
src={brewery.breweryImages[0].path}
|
||||
alt={brewery.name}
|
||||
width="1029"
|
||||
height="110"
|
||||
crop="fill"
|
||||
className="h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import sendUploadBeerImagesRequest from '@/requests/BeerImage/sendUploadBeerImag
|
||||
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import Button from './ui/forms/Button';
|
||||
import FormError from './ui/forms/FormError';
|
||||
import FormInfo from './ui/forms/FormInfo';
|
||||
@@ -51,14 +52,14 @@ const CreateBeerPostForm: FunctionComponent<BeerFormProps> = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const loadingToast = toast.loading('Creating beer post...');
|
||||
const beerPost = await sendCreateBeerPostRequest(data);
|
||||
await sendUploadBeerImagesRequest({ beerPost, images: data.images });
|
||||
await router.push(`/beers/${beerPost.id}`);
|
||||
toast.dismiss(loadingToast);
|
||||
toast.success('Created beer post.');
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
||||
|
||||
toast.error(errorMessage);
|
||||
createErrorToast(e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import deleteBeerPostRequest from '@/requests/BeerPost/deleteBeerPostRequest';
|
||||
import EditBeerPostValidationSchema from '@/services/BeerPost/schema/EditBeerPostValidationSchema';
|
||||
import sendEditBeerPostRequest from '@/requests/BeerPost/sendEditBeerPostRequest';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import Button from './ui/forms/Button';
|
||||
import FormError from './ui/forms/FormError';
|
||||
import FormInfo from './ui/forms/FormInfo';
|
||||
@@ -33,23 +34,26 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
|
||||
const { isSubmitting, errors } = formState;
|
||||
const onSubmit: SubmitHandler<EditBeerPostSchema> = async (data) => {
|
||||
try {
|
||||
const loadingToast = toast.loading('Editing beer post...');
|
||||
await sendEditBeerPostRequest(data);
|
||||
await router.push(`/beers/${data.id}`);
|
||||
toast.success('Edited beer post.');
|
||||
toast.dismiss(loadingToast);
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
||||
toast.error(errorMessage);
|
||||
createErrorToast(e);
|
||||
await router.push(`/beers/${data.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
const loadingToast = toast.loading('Deleting beer post...');
|
||||
await deleteBeerPostRequest(previousValues.id);
|
||||
toast.dismiss(loadingToast);
|
||||
await router.push('/beers');
|
||||
toast.success('Deleted beer post.');
|
||||
} catch (e) {
|
||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
||||
toast.error(errorMessage);
|
||||
createErrorToast(e);
|
||||
await router.push(`/beers`);
|
||||
}
|
||||
};
|
||||
|
||||
66
src/components/UserPage/UserFollowButton.tsx
Normal file
66
src/components/UserPage/UserFollowButton.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import useFollowStatus from '@/hooks/data-fetching/user-follows/useFollowStatus';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
import sendUserFollowRequest from '@/requests/UserFollow/sendUserFollowRequest';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import { FC, useState } from 'react';
|
||||
import { FaUserCheck, FaUserPlus } from 'react-icons/fa';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface UserFollowButtonProps {
|
||||
mutateFollowerCount: ReturnType<typeof useGetUsersFollowingUser>['mutate'];
|
||||
mutateFollowingCount: ReturnType<typeof useGetUsersFollowedByUser>['mutate'];
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
|
||||
const UserFollowButton: FC<UserFollowButtonProps> = ({
|
||||
user,
|
||||
mutateFollowerCount,
|
||||
mutateFollowingCount,
|
||||
}) => {
|
||||
const { isFollowed, mutate: mutateFollowStatus } = useFollowStatus(user.id);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onClick = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await sendUserFollowRequest(user.id);
|
||||
await Promise.all([
|
||||
mutateFollowStatus(),
|
||||
mutateFollowerCount(),
|
||||
mutateFollowingCount(),
|
||||
]);
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn-sm btn gap-2 rounded-2xl lg:btn-md ${
|
||||
!isFollowed ? 'btn-ghost outline' : 'btn-primary'
|
||||
}`}
|
||||
onClick={() => {
|
||||
onClick();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isFollowed ? (
|
||||
<>
|
||||
<FaUserCheck className="text-xl" />
|
||||
Followed
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaUserPlus className="text-xl" />
|
||||
Follow
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserFollowButton;
|
||||
@@ -1,20 +1,34 @@
|
||||
import useTimeDistance from '@/hooks/utilities/useTimeDistance';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { FC, useContext } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { format } from 'date-fns';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
import UserContext from '@/contexts/UserContext';
|
||||
import Link from 'next/link';
|
||||
import UserAvatar from '../Account/UserAvatar';
|
||||
import UserFollowButton from './UserFollowButton';
|
||||
|
||||
interface UserHeaderProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
followerCount: ReturnType<typeof useGetUsersFollowingUser>['followerCount'];
|
||||
followingCount: ReturnType<typeof useGetUsersFollowedByUser>['followingCount'];
|
||||
}
|
||||
const UserHeader: FC<UserHeaderProps> = ({ user, followerCount, followingCount }) => {
|
||||
const UserHeader: FC<UserHeaderProps> = ({ user }) => {
|
||||
const timeDistance = useTimeDistance(new Date(user.createdAt));
|
||||
|
||||
const { followingCount, mutate: mutateFollowingCount } = useGetUsersFollowedByUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { followerCount, mutate: mutateFollowerCount } = useGetUsersFollowingUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { user: currentUser } = useContext(UserContext);
|
||||
|
||||
return (
|
||||
<header className="card text-center items-center">
|
||||
<div className="card-body items-center w-full">
|
||||
@@ -42,6 +56,25 @@ const UserHeader: FC<UserHeaderProps> = ({ user, followerCount, followingCount }
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<div className="w-6/12">
|
||||
<p className="text-sm">{user.bio}</p>
|
||||
</div>
|
||||
|
||||
{currentUser?.id !== user.id ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<UserFollowButton
|
||||
mutateFollowerCount={mutateFollowerCount}
|
||||
user={user}
|
||||
mutateFollowingCount={mutateFollowingCount}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center">
|
||||
<Link href={`/account/profile`} className="btn btn-primary">
|
||||
Edit Profile
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
95
src/config/cloudinary/CloudinaryStorage.ts
Normal file
95
src/config/cloudinary/CloudinaryStorage.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import type { StorageEngine } from 'multer';
|
||||
import type { UploadApiOptions, UploadApiResponse, v2 as cloudinary } from 'cloudinary';
|
||||
import type { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Represents a storage engine for uploading files to Cloudinary.
|
||||
*
|
||||
* @example
|
||||
* const storage = new CloudinaryStorage({
|
||||
* cloudinary,
|
||||
* params: {
|
||||
* folder: 'my-folder',
|
||||
* allowed_formats: ['jpg', 'png'],
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
class CloudinaryStorage implements StorageEngine {
|
||||
private cloudinary: typeof cloudinary;
|
||||
|
||||
private params: UploadApiOptions;
|
||||
|
||||
/**
|
||||
* Creates an instance of CloudinaryStorage.
|
||||
*
|
||||
* @param options - The options for configuring the Cloudinary storage engine.
|
||||
* @param options.cloudinary - The Cloudinary instance.
|
||||
* @param options.params - The parameters for uploading files to Cloudinary.
|
||||
*/
|
||||
constructor(options: { cloudinary: typeof cloudinary; params: UploadApiOptions }) {
|
||||
this.cloudinary = options.cloudinary;
|
||||
this.params = options.params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file from Cloudinary.
|
||||
*
|
||||
* @param req - The request object.
|
||||
* @param file - The file to be removed.
|
||||
* @param callback - The callback function to be called if an error occurs.
|
||||
*/
|
||||
_removeFile(req: Request, file: Express.Multer.File, callback: (error: Error) => void) {
|
||||
this.cloudinary.uploader.destroy(file.filename, { invalidate: true }, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the file upload to Cloudinary.
|
||||
*
|
||||
* @param req - The request object.
|
||||
* @param file - The file to be uploaded.
|
||||
* @param callback - The callback function to be called after the file is uploaded.
|
||||
*/
|
||||
_handleFile(
|
||||
req: Request,
|
||||
file: Express.Multer.File,
|
||||
callback: (error?: unknown, info?: Partial<Express.Multer.File>) => void,
|
||||
) {
|
||||
this.uploadFile(file)
|
||||
.then((cloudResponse) => {
|
||||
callback(null, {
|
||||
path: cloudResponse.secure_url,
|
||||
size: cloudResponse.bytes,
|
||||
filename: cloudResponse.public_id,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
callback(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file to Cloudinary.
|
||||
*
|
||||
* @param file - The file to be uploaded.
|
||||
* @returns A promise that resolves to the upload response.
|
||||
*/
|
||||
private uploadFile(file: Express.Multer.File): Promise<UploadApiResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = this.cloudinary.uploader.upload_stream(
|
||||
this.params,
|
||||
(err, response) => {
|
||||
if (err != null) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(response!);
|
||||
},
|
||||
);
|
||||
|
||||
file.stream.pipe(stream);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CloudinaryStorage;
|
||||
@@ -1,19 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { v2 as cloudinary } from 'cloudinary';
|
||||
import { CloudinaryStorage } from 'multer-storage-cloudinary';
|
||||
|
||||
import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_KEY, CLOUDINARY_SECRET } from '../env';
|
||||
import {
|
||||
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
|
||||
CLOUDINARY_KEY,
|
||||
CLOUDINARY_SECRET,
|
||||
} from '../env';
|
||||
import CloudinaryStorage from './CloudinaryStorage';
|
||||
|
||||
cloudinary.config({
|
||||
cloud_name: CLOUDINARY_CLOUD_NAME,
|
||||
cloud_name: NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
|
||||
api_key: CLOUDINARY_KEY,
|
||||
api_secret: CLOUDINARY_SECRET,
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'BeerApp' } });
|
||||
/** Cloudinary storage instance. */
|
||||
const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'biergarten' } });
|
||||
|
||||
/** Configuration object for Cloudinary image upload. */
|
||||
const cloudinaryConfig = { cloudinary, storage };
|
||||
|
||||
export default cloudinaryConfig;
|
||||
export { cloudinary, storage };
|
||||
|
||||
5
src/config/env/index.ts
vendored
5
src/config/env/index.ts
vendored
@@ -11,7 +11,7 @@ import 'dotenv/config';
|
||||
*/
|
||||
const envSchema = z.object({
|
||||
BASE_URL: z.string().url(),
|
||||
CLOUDINARY_CLOUD_NAME: z.string(),
|
||||
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: z.string(),
|
||||
CLOUDINARY_KEY: z.string(),
|
||||
CLOUDINARY_SECRET: z.string(),
|
||||
RESET_PASSWORD_TOKEN_SECRET: z.string(),
|
||||
@@ -56,7 +56,8 @@ export const BASE_URL = parsed.data.BASE_URL;
|
||||
* @see https://cloudinary.com/console
|
||||
*/
|
||||
|
||||
export const CLOUDINARY_CLOUD_NAME = parsed.data.CLOUDINARY_CLOUD_NAME;
|
||||
export const NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME =
|
||||
parsed.data.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
|
||||
|
||||
/**
|
||||
* Cloudinary API key.
|
||||
|
||||
28
src/config/multer/uploadMiddleware.ts
Normal file
28
src/config/multer/uploadMiddleware.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import multer from 'multer';
|
||||
import { expressWrapper } from 'next-connect';
|
||||
import { storage } from '../cloudinary';
|
||||
|
||||
const fileFilter: multer.Options['fileFilter'] = (req, file, callback) => {
|
||||
const { mimetype } = file;
|
||||
|
||||
const isImage = mimetype.startsWith('image/');
|
||||
|
||||
if (!isImage) {
|
||||
callback(null, false);
|
||||
}
|
||||
callback(null, true);
|
||||
};
|
||||
|
||||
export const uploadMiddlewareMultiple = expressWrapper(
|
||||
multer({ storage, fileFilter, limits: { files: 5, fileSize: 15 * 1024 * 1024 } }).array(
|
||||
'images',
|
||||
),
|
||||
);
|
||||
|
||||
export const singleUploadMiddleware = expressWrapper(
|
||||
multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: { files: 1, fileSize: 15 * 1024 * 1024 },
|
||||
}).single('image'),
|
||||
);
|
||||
@@ -33,6 +33,7 @@ const UseBeerPostsByBrewery = ({ pageSize, breweryId }: UseBeerPostsByBreweryPar
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
const count = response.headers.get('X-Total-Count');
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
38
src/hooks/data-fetching/user-follows/useFollowStatus.ts
Normal file
38
src/hooks/data-fetching/user-follows/useFollowStatus.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import useSWR from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
||||
const useFollowStatus = (userFollowedId: string) => {
|
||||
const { data, error, isLoading, mutate } = useSWR(
|
||||
`/api/users/${userFollowedId}/is-followed`,
|
||||
async (url) => {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid API response.');
|
||||
}
|
||||
|
||||
const { payload } = parsed.data;
|
||||
const parsedPayload = z.object({ isFollowed: z.boolean() }).safeParse(payload);
|
||||
|
||||
if (!parsedPayload.success) {
|
||||
throw new Error('Invalid API response.');
|
||||
}
|
||||
|
||||
const { isFollowed } = parsedPayload.data;
|
||||
|
||||
return isFollowed;
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
isFollowed: data,
|
||||
error: error as unknown,
|
||||
isLoading,
|
||||
mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFollowStatus;
|
||||
@@ -34,7 +34,7 @@ const useGetUsersFollowedByUser = ({
|
||||
return { following: parsedPayload.data, pageCount, followingCount: count };
|
||||
};
|
||||
|
||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||
const { data, error, isLoading, setSize, size, mutate } = useSWRInfinite(
|
||||
(index) =>
|
||||
`/api/users/${userId}/following?page_num=${index + 1}&page_size=${pageSize}`,
|
||||
fetcher,
|
||||
@@ -57,6 +57,7 @@ const useGetUsersFollowedByUser = ({
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isAtEnd,
|
||||
mutate,
|
||||
error: error as unknown,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ const useGetUsersFollowingUser = ({
|
||||
return { followers: parsedPayload.data, pageCount, followerCount: count };
|
||||
};
|
||||
|
||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
||||
const { data, error, isLoading, setSize, size, mutate } = useSWRInfinite(
|
||||
(index) =>
|
||||
`/api/users/${userId}/followers?page_num=${index + 1}&page_size=${pageSize}`,
|
||||
fetcher,
|
||||
@@ -57,6 +57,7 @@ const useGetUsersFollowingUser = ({
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
isAtEnd,
|
||||
mutate,
|
||||
error: error as unknown,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ const AccountPage: NextPage = () => {
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-8/12">
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<div className="h-20 mb-10 w-20">
|
||||
<div className="h-28 mb-1 w-28">
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
|
||||
|
||||
157
src/pages/account/profile.tsx
Normal file
157
src/pages/account/profile.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import FormError from '@/components/ui/forms/FormError';
|
||||
import FormInfo from '@/components/ui/forms/FormInfo';
|
||||
import FormLabel from '@/components/ui/forms/FormLabel';
|
||||
import FormSegment from '@/components/ui/forms/FormSegment';
|
||||
import FormTextInput from '@/components/ui/forms/FormTextInput';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
||||
import { GetServerSideProps, NextPage } from 'next';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
import createErrorToast from '@/util/createErrorToast';
|
||||
import Button from '@/components/ui/forms/Button';
|
||||
|
||||
interface ProfilePageProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
}
|
||||
|
||||
const UpdateProfileSchema = z.object({
|
||||
bio: z.string().min(1, 'Bio cannot be empty'),
|
||||
userAvatar: z
|
||||
.instanceof(typeof FileList !== 'undefined' ? FileList : Object)
|
||||
.refine((fileList) => fileList instanceof FileList, {
|
||||
message: 'You must submit this form in a web browser.',
|
||||
})
|
||||
.refine((fileList) => (fileList as FileList).length === 1, {
|
||||
message: 'You must upload exactly one file.',
|
||||
})
|
||||
.refine(
|
||||
(fileList) =>
|
||||
[...(fileList as FileList)]
|
||||
.map((file) => file.type)
|
||||
.every((fileType) => fileType.startsWith('image/')),
|
||||
{ message: 'You must upload only images.' },
|
||||
)
|
||||
.refine(
|
||||
(fileList) =>
|
||||
[...(fileList as FileList)]
|
||||
.map((file) => file.size)
|
||||
.every((fileSize) => fileSize < 15 * 1024 * 1024),
|
||||
{ message: 'You must upload images smaller than 15MB.' },
|
||||
),
|
||||
});
|
||||
|
||||
const sendUpdateProfileRequest = async (data: z.infer<typeof UpdateProfileSchema>) => {
|
||||
if (!(data.userAvatar instanceof FileList)) {
|
||||
throw new Error('You must submit this form in a web browser.');
|
||||
}
|
||||
|
||||
const { bio, userAvatar } = data;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('image', userAvatar[0]);
|
||||
formData.append('bio', bio);
|
||||
|
||||
const response = await fetch(`/api/users/profile`, {
|
||||
method: 'PUT',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
|
||||
const updatedUser = await response.json();
|
||||
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
const ProfilePage: NextPage<ProfilePageProps> = ({ user }) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
reset,
|
||||
} = useForm<z.infer<typeof UpdateProfileSchema>>({
|
||||
resolver: zodResolver(UpdateProfileSchema),
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof UpdateProfileSchema>> = async (data) => {
|
||||
try {
|
||||
await sendUpdateProfileRequest(data);
|
||||
const loadingToast = toast.loading('Updating profile...');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
toast.remove(loadingToast);
|
||||
// reset();
|
||||
toast.success('Profile updated!');
|
||||
} catch (error) {
|
||||
createErrorToast(error);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-9/12">
|
||||
<pre>{JSON.stringify(user, null, 2)}</pre>
|
||||
<form className="form-control" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="bio">Bio</FormLabel>
|
||||
<FormError>{errors.bio?.message}</FormError>
|
||||
</FormInfo>
|
||||
|
||||
<FormSegment>
|
||||
<FormTextInput
|
||||
disabled={isSubmitting}
|
||||
id="bio"
|
||||
type="text"
|
||||
formValidationSchema={register('bio')}
|
||||
error={!!errors.bio}
|
||||
placeholder="Bio"
|
||||
/>
|
||||
</FormSegment>
|
||||
|
||||
<FormInfo>
|
||||
<FormLabel htmlFor="userAvatar">Avatar</FormLabel>
|
||||
<FormError>{errors.userAvatar?.message}</FormError>
|
||||
</FormInfo>
|
||||
<FormSegment>
|
||||
<input
|
||||
disabled={isSubmitting}
|
||||
type="file"
|
||||
id="userAvatar"
|
||||
className="file-input-bordered file-input w-full"
|
||||
{...register('userAvatar')}
|
||||
/>
|
||||
</FormSegment>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="submit" isSubmitting={isSubmitting}>
|
||||
Update Profile
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps =
|
||||
withPageAuthRequired<ProfilePageProps>(async (context, session) => {
|
||||
const { id } = session;
|
||||
|
||||
const user = await findUserById(id);
|
||||
|
||||
if (!user) {
|
||||
return { notFound: true };
|
||||
}
|
||||
|
||||
return { props: { user: JSON.parse(JSON.stringify(user)) } };
|
||||
});
|
||||
@@ -1,38 +1,17 @@
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { createRouter, expressWrapper } from 'next-connect';
|
||||
import { createRouter } 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 addBeerImageToDB from '@/services/BeerImage/addBeerImageToDB';
|
||||
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
||||
|
||||
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',
|
||||
),
|
||||
);
|
||||
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||
|
||||
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
|
||||
files?: Express.Multer.File[];
|
||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
||||
router.post(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
uploadMiddleware,
|
||||
uploadMiddlewareMultiple,
|
||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||
processImageData,
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ const editBeerPost = async (
|
||||
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const deleted = deleteBeerPostById({ beerPostId: id });
|
||||
const deleted = await deleteBeerPostById({ beerPostId: id });
|
||||
if (!deleted) {
|
||||
throw new ServerError('Beer post not found', 404);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ const getAllBeersByBrewery = async (
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { page_size, page_num, id } = req.query;
|
||||
|
||||
const pageNum = parseInt(page_num, 10);
|
||||
const pageSize = parseInt(page_size, 10);
|
||||
|
||||
const beers: z.infer<typeof BeerPostQueryResult>[] =
|
||||
await DBClient.instance.beerPost.findMany({
|
||||
where: { breweryId: id },
|
||||
take: parseInt(page_size, 10),
|
||||
skip: parseInt(page_num, 10) * parseInt(page_size, 10),
|
||||
skip: (pageNum - 1) * pageSize,
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
|
||||
@@ -1,38 +1,17 @@
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { createRouter, expressWrapper } from 'next-connect';
|
||||
import { createRouter } 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/schema/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',
|
||||
),
|
||||
);
|
||||
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||
|
||||
interface UploadBreweryPostImagesRequest extends UserExtendedNextApiRequest {
|
||||
files?: Express.Multer.File[];
|
||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
||||
router.post(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
uploadMiddleware,
|
||||
uploadMiddlewareMultiple,
|
||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||
processImageData,
|
||||
);
|
||||
|
||||
81
src/pages/api/users/[id]/follow-user.ts
Normal file
81
src/pages/api/users/[id]/follow-user.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string };
|
||||
}
|
||||
|
||||
const router = createRouter<
|
||||
GetUserFollowInfoRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
const followUser = async (
|
||||
req: GetUserFollowInfoRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const user = await findUserById(id);
|
||||
if (!user) {
|
||||
throw new ServerError('User not found', 404);
|
||||
}
|
||||
|
||||
const currentUser = req.user!;
|
||||
const userIsFollowedBySessionUser = await DBClient.instance.userFollow.findFirst({
|
||||
where: {
|
||||
followerId: currentUser.id,
|
||||
followingId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userIsFollowedBySessionUser) {
|
||||
await DBClient.instance.userFollow.create({
|
||||
data: { followerId: currentUser.id, followingId: id },
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Now following user.',
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await DBClient.instance.userFollow.delete({
|
||||
where: {
|
||||
followerId_followingId: {
|
||||
followerId: currentUser.id,
|
||||
followingId: id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
message: 'No longer following user.',
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
});
|
||||
};
|
||||
|
||||
router.post(
|
||||
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||
getCurrentUser,
|
||||
followUser,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
|
||||
export default handler;
|
||||
71
src/pages/api/users/[id]/is-followed.ts
Normal file
71
src/pages/api/users/[id]/is-followed.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import ServerError from '@/config/util/ServerError';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import findUserById from '@/services/User/findUserById';
|
||||
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface GetUserFollowInfoRequest extends UserExtendedNextApiRequest {
|
||||
query: { id: string };
|
||||
}
|
||||
|
||||
const router = createRouter<
|
||||
GetUserFollowInfoRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
const checkIfUserIsFollowedBySessionUser = async (
|
||||
req: GetUserFollowInfoRequest,
|
||||
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
|
||||
) => {
|
||||
const { id } = req.query;
|
||||
|
||||
const user = await findUserById(id);
|
||||
if (!user) {
|
||||
throw new ServerError('User not found', 404);
|
||||
}
|
||||
|
||||
const currentUser = req.user!;
|
||||
|
||||
const userIsFollowedBySessionUser = await DBClient.instance.userFollow.findFirst({
|
||||
where: {
|
||||
followerId: currentUser.id,
|
||||
followingId: id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userIsFollowedBySessionUser) {
|
||||
res.status(200).json({
|
||||
message: 'User is not followed by the current user.',
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
payload: { isFollowed: false },
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: 'User is followed by the current user.',
|
||||
success: true,
|
||||
statusCode: 200,
|
||||
payload: { isFollowed: true },
|
||||
});
|
||||
};
|
||||
|
||||
router.get(
|
||||
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
|
||||
getCurrentUser,
|
||||
checkIfUserIsFollowedBySessionUser,
|
||||
);
|
||||
|
||||
const handler = router.handler(NextConnectOptions);
|
||||
|
||||
export default handler;
|
||||
99
src/pages/api/users/profile.ts
Normal file
99
src/pages/api/users/profile.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
||||
import { singleUploadMiddleware } from '@/config/multer/uploadMiddleware';
|
||||
import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
|
||||
import DBClient from '@/prisma/DBClient';
|
||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
import { NextApiResponse } from 'next';
|
||||
import { createRouter } from 'next-connect';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface UpdateProfileRequest extends UserExtendedNextApiRequest {
|
||||
file?: Express.Multer.File;
|
||||
body: {
|
||||
bio: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserProfileByIdParams {
|
||||
id: string;
|
||||
data: {
|
||||
bio: string;
|
||||
avatar: {
|
||||
alt: string;
|
||||
path: string;
|
||||
caption: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const updateUserProfileById = async ({ id, data }: UpdateUserProfileByIdParams) => {
|
||||
const { alt, path, caption } = data.avatar;
|
||||
const user: z.infer<typeof GetUserSchema> = await DBClient.instance.user.update({
|
||||
where: { id },
|
||||
data: {
|
||||
bio: data.bio,
|
||||
userAvatar: {
|
||||
upsert: { create: { alt, path, caption }, update: { alt, path, caption } },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
bio: true,
|
||||
userAvatar: true,
|
||||
accountIsVerified: true,
|
||||
createdAt: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
updatedAt: true,
|
||||
dateOfBirth: true,
|
||||
role: true,
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const updateProfile = async (req: UpdateProfileRequest, res: NextApiResponse) => {
|
||||
const { file, body, user } = req;
|
||||
|
||||
if (!file) {
|
||||
throw new Error('No file uploaded');
|
||||
}
|
||||
|
||||
await updateUserProfileById({
|
||||
id: user!.id,
|
||||
data: {
|
||||
bio: body.bio,
|
||||
avatar: { alt: file.originalname, path: file.path, caption: '' },
|
||||
},
|
||||
});
|
||||
res.status(200).json({
|
||||
message: 'User confirmed successfully.',
|
||||
statusCode: 200,
|
||||
success: true,
|
||||
});
|
||||
};
|
||||
|
||||
const router = createRouter<
|
||||
UpdateProfileRequest,
|
||||
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
|
||||
>();
|
||||
|
||||
router.put(
|
||||
getCurrentUser,
|
||||
// @ts-expect-error
|
||||
singleUploadMiddleware,
|
||||
|
||||
validateRequest({ bodySchema: z.object({ bio: z.string().max(1000) }) }),
|
||||
updateProfile,
|
||||
);
|
||||
|
||||
const handler = router.handler();
|
||||
|
||||
export default handler;
|
||||
export const config = { api: { bodyParser: false } };
|
||||
@@ -1,7 +1,5 @@
|
||||
import { NextPage, GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
|
||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||
|
||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||
@@ -13,6 +11,7 @@ import { Carousel } from 'react-responsive-carousel';
|
||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||
import { Tab } from '@headlessui/react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
|
||||
const [BeerInfoHeader, BeerPostCommentsSection, BeerRecommendations] = [
|
||||
dynamic(() => import('@/components/BeerById/BeerInfoHeader')),
|
||||
@@ -45,10 +44,11 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
|
||||
{beerPost.beerImages.length
|
||||
? beerPost.beerImages.map((image, index) => (
|
||||
<div key={image.id} id={`image-${index}}`} className="w-full">
|
||||
<Image
|
||||
<CldImage
|
||||
alt={image.alt}
|
||||
src={image.path}
|
||||
height={1080}
|
||||
crop="fill"
|
||||
width={1920}
|
||||
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GetServerSideProps, NextPage } from 'next';
|
||||
|
||||
import { z } from 'zod';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
|
||||
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
||||
import { Carousel } from 'react-responsive-carousel';
|
||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||
@@ -12,6 +12,7 @@ import { Tab } from '@headlessui/react';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
|
||||
const [BreweryInfoHeader, BreweryBeersSection, BreweryCommentsSection, BreweryPostMap] = [
|
||||
dynamic(() => import('@/components/BreweryById/BreweryInfoHeader')),
|
||||
@@ -47,9 +48,10 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost, mapboxToken
|
||||
{breweryPost.breweryImages.length
|
||||
? breweryPost.breweryImages.map((image, index) => (
|
||||
<div key={image.id} id={`image-${index}}`} className="w-full">
|
||||
<Image
|
||||
<CldImage
|
||||
alt={image.alt}
|
||||
src={image.path}
|
||||
crop="fill"
|
||||
height={1080}
|
||||
width={1920}
|
||||
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { NextPage } from 'next';
|
||||
|
||||
import LoginForm from '@/components/Login/LoginForm';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { FaUserCircle } from 'react-icons/fa';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
|
||||
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
|
||||
import { CldImage } from 'next-cloudinary';
|
||||
|
||||
const LoginPage: NextPage = () => {
|
||||
useRedirectWhenLoggedIn();
|
||||
@@ -20,11 +20,11 @@ const LoginPage: NextPage = () => {
|
||||
|
||||
<div className="flex h-full flex-row">
|
||||
<div className="hidden h-full flex-col items-center justify-center bg-base-100 lg:flex lg:w-[55%]">
|
||||
<Image
|
||||
src="https://picsum.photos/5000/5000"
|
||||
<CldImage
|
||||
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
|
||||
alt="Login Image"
|
||||
width={4920}
|
||||
height={4080}
|
||||
width={5000}
|
||||
height={5000}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,6 @@ import { FC } from 'react';
|
||||
import { z } from 'zod';
|
||||
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
||||
import UserHeader from '@/components/UserPage/UserHeader';
|
||||
import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowedByUser';
|
||||
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
|
||||
|
||||
interface UserInfoPageProps {
|
||||
user: z.infer<typeof GetUserSchema>;
|
||||
@@ -19,16 +17,6 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||
const title = `${user.username} | The Biergarten App`;
|
||||
|
||||
const { followingCount } = useGetUsersFollowedByUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { followerCount } = useGetUsersFollowingUser({
|
||||
userId: user.id,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -36,34 +24,9 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
||||
<meta name="description" content="User information" />
|
||||
</Head>
|
||||
<>
|
||||
<main className="mb-12 mt-10 flex w-full items-center justify-center">
|
||||
<div className="h-full w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||
<UserHeader
|
||||
user={user}
|
||||
followerCount={followerCount}
|
||||
followingCount={followingCount}
|
||||
/>
|
||||
|
||||
{isDesktop ? (
|
||||
<div className="h-64 flex space-x-3">
|
||||
<div className="h-full w-5/12">
|
||||
<div className="h-full card">
|
||||
<div className="card-body">
|
||||
<h2 className="text-2xl font-bold">About Me</h2>
|
||||
<p>{user.bio}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-7/12">
|
||||
<div className="h-full card">
|
||||
<div className="h-full card-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<main className="mb-12 mt-10 flex flex-col w-full items-center justify-center">
|
||||
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||
<UserHeader user={user} />
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
|
||||
@@ -75,7 +75,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
||||
|
||||
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
||||
const createdAt = faker.date.past({ years: 4 });
|
||||
const bio = faker.lorem.paragraphs(3).replace(/\n/g, ' ');
|
||||
const bio = faker.lorem.paragraphs(1).replace(/\n/g, ' ');
|
||||
|
||||
const user: UserData = {
|
||||
firstName,
|
||||
|
||||
16
src/requests/UserFollow/sendUserFollowRequest.ts
Normal file
16
src/requests/UserFollow/sendUserFollowRequest.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||
|
||||
const sendUserFollowRequest = async (userId: string) => {
|
||||
const response = await fetch(`/api/users/${userId}/follow-user`, { method: 'POST' });
|
||||
const json = await response.json();
|
||||
|
||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new Error('Invalid API response.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
};
|
||||
|
||||
export default sendUserFollowRequest;
|
||||
Reference in New Issue
Block a user