mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +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
|
```bash
|
||||||
echo "BASE_URL=
|
echo "BASE_URL=
|
||||||
CLOUDINARY_CLOUD_NAME=
|
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=
|
||||||
CLOUDINARY_KEY=
|
CLOUDINARY_KEY=
|
||||||
CLOUDINARY_SECRET=
|
CLOUDINARY_SECRET=
|
||||||
CONFIRMATION_TOKEN_SECRET=
|
CONFIRMATION_TOKEN_SECRET=
|
||||||
@@ -149,8 +149,8 @@ SPARKPOST_SENDER_ADDRESS=" > .env
|
|||||||
- `BASE_URL` is the base URL of the application.
|
- `BASE_URL` is the base URL of the application.
|
||||||
- For example, if you are running the application locally, you can set this to
|
- For example, if you are running the application locally, you can set this to
|
||||||
`http://localhost:3000`.
|
`http://localhost:3000`.
|
||||||
- `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the credentials
|
- `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_KEY`, and `CLOUDINARY_SECRET` are the
|
||||||
for your Cloudinary account.
|
credentials for your Cloudinary account.
|
||||||
- You can create a free account [here](https://cloudinary.com/users/register/free).
|
- 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
|
- `CONFIRMATION_TOKEN_SECRET` is the secret used to sign the confirmation token used for
|
||||||
email confirmation.
|
email confirmation.
|
||||||
|
|||||||
147
package-lock.json
generated
147
package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"@react-email/components": "^0.0.11",
|
"@react-email/components": "^0.0.11",
|
||||||
"@react-email/render": "^0.0.9",
|
"@react-email/render": "^0.0.9",
|
||||||
"@react-email/tailwind": "^0.0.12",
|
"@react-email/tailwind": "^0.0.12",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@vercel/analytics": "^1.1.0",
|
"@vercel/analytics": "^1.1.0",
|
||||||
"argon2": "^0.31.1",
|
"argon2": "^0.31.1",
|
||||||
"cloudinary": "^1.41.0",
|
"cloudinary": "^1.41.0",
|
||||||
@@ -30,8 +31,8 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mapbox-gl": "^2.15.0",
|
"mapbox-gl": "^2.15.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
|
||||||
"next": "^14.0.3",
|
"next": "^14.0.3",
|
||||||
|
"next-cloudinary": "^5.10.0",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -289,6 +290,36 @@
|
|||||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -2030,7 +2061,6 @@
|
|||||||
"version": "1.19.3",
|
"version": "1.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
||||||
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2057,7 +2087,6 @@
|
|||||||
"version": "3.4.36",
|
"version": "3.4.36",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
||||||
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -2078,10 +2107,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/express": {
|
"node_modules/@types/express": {
|
||||||
"version": "4.17.18",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
"integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==",
|
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^4.17.33",
|
"@types/express-serve-static-core": "^4.17.33",
|
||||||
@@ -2093,7 +2121,6 @@
|
|||||||
"version": "4.17.37",
|
"version": "4.17.37",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
|
"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==",
|
"integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/qs": "*",
|
"@types/qs": "*",
|
||||||
@@ -2114,8 +2141,7 @@
|
|||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
|
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
@@ -2190,8 +2216,7 @@
|
|||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
|
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/minimist": {
|
"node_modules/@types/minimist": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
@@ -2285,14 +2310,12 @@
|
|||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.8",
|
"version": "6.9.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
||||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
|
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/range-parser": {
|
"node_modules/@types/range-parser": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
||||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
|
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.2.25",
|
"version": "18.2.25",
|
||||||
@@ -2370,7 +2393,6 @@
|
|||||||
"version": "0.17.2",
|
"version": "0.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
||||||
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -2380,7 +2402,6 @@
|
|||||||
"version": "1.15.3",
|
"version": "1.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
||||||
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
"@types/mime": "*",
|
"@types/mime": "*",
|
||||||
@@ -7474,14 +7495,6 @@
|
|||||||
"node": ">= 6.0.0"
|
"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": {
|
"node_modules/murmurhash-js": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
"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": {
|
"node_modules/next-connect": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz",
|
||||||
@@ -10941,6 +10967,36 @@
|
|||||||
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
"integrity": "sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==",
|
||||||
"dev": true
|
"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": {
|
"@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -12079,7 +12135,6 @@
|
|||||||
"version": "1.19.3",
|
"version": "1.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz",
|
||||||
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
"integrity": "sha512-oyl4jvAfTGX9Bt6Or4H9ni1Z447/tQuxnZsytsCaExKlmJiU8sFgnIBRzJUpKwB5eWn9HuBYlUlVA74q/yN0eQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/connect": "*",
|
"@types/connect": "*",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -12106,7 +12161,6 @@
|
|||||||
"version": "3.4.36",
|
"version": "3.4.36",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
|
||||||
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
"integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@@ -12127,10 +12181,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/express": {
|
"@types/express": {
|
||||||
"version": "4.17.18",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||||
"integrity": "sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==",
|
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^4.17.33",
|
"@types/express-serve-static-core": "^4.17.33",
|
||||||
@@ -12142,7 +12195,6 @@
|
|||||||
"version": "4.17.37",
|
"version": "4.17.37",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.37.tgz",
|
"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==",
|
"integrity": "sha512-ZohaCYTgGFcOP7u6aJOhY9uIZQgZ2vxC2yWoArY+FeDXlqeH66ZVBjgvg+RLVAS/DWNq4Ap9ZXu1+SUQiiWYMg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/qs": "*",
|
"@types/qs": "*",
|
||||||
@@ -12163,8 +12215,7 @@
|
|||||||
"@types/http-errors": {
|
"@types/http-errors": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
|
||||||
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg==",
|
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/json-schema": {
|
"@types/json-schema": {
|
||||||
"version": "7.0.13",
|
"version": "7.0.13",
|
||||||
@@ -12239,8 +12290,7 @@
|
|||||||
"@types/mime": {
|
"@types/mime": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||||
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==",
|
"integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/minimist": {
|
"@types/minimist": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
@@ -12333,14 +12383,12 @@
|
|||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.8",
|
"version": "6.9.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.8.tgz",
|
||||||
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==",
|
"integrity": "sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/range-parser": {
|
"@types/range-parser": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.5.tgz",
|
||||||
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA==",
|
"integrity": "sha512-xrO9OoVPqFuYyR/loIHjnbvvyRZREYKLjxV4+dY6v3FQR3stQ9ZxIGkaclF7YhI9hfjpuTbu14hZEy94qKLtOA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
"version": "18.2.25",
|
"version": "18.2.25",
|
||||||
@@ -12417,7 +12465,6 @@
|
|||||||
"version": "0.17.2",
|
"version": "0.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.2.tgz",
|
||||||
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
"integrity": "sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/mime": "^1",
|
"@types/mime": "^1",
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -12427,7 +12474,6 @@
|
|||||||
"version": "1.15.3",
|
"version": "1.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.3.tgz",
|
||||||
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
"integrity": "sha512-yVRvFsEMrv7s0lGhzrggJjNOSmZCdgCjw9xWrPr/kNNLp6FaDfMC1KaYl3TSJ0c58bECwNBMoQrZJ8hA8E1eFg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/http-errors": "*",
|
"@types/http-errors": "*",
|
||||||
"@types/mime": "*",
|
"@types/mime": "*",
|
||||||
@@ -16062,12 +16108,6 @@
|
|||||||
"xtend": "^4.0.0"
|
"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": {
|
"murmurhash-js": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||||
@@ -16117,6 +16157,15 @@
|
|||||||
"watchpack": "2.4.0"
|
"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": {
|
"next-connect": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0.tgz",
|
"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/components": "^0.0.11",
|
||||||
"@react-email/render": "^0.0.9",
|
"@react-email/render": "^0.0.9",
|
||||||
"@react-email/tailwind": "^0.0.12",
|
"@react-email/tailwind": "^0.0.12",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@vercel/analytics": "^1.1.0",
|
"@vercel/analytics": "^1.1.0",
|
||||||
"argon2": "^0.31.1",
|
"argon2": "^0.31.1",
|
||||||
"cloudinary": "^1.41.0",
|
"cloudinary": "^1.41.0",
|
||||||
@@ -35,8 +36,8 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mapbox-gl": "^2.15.0",
|
"mapbox-gl": "^2.15.0",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"multer-storage-cloudinary": "^4.0.0",
|
|
||||||
"next": "^14.0.3",
|
"next": "^14.0.3",
|
||||||
|
"next-cloudinary": "^5.10.0",
|
||||||
"next-connect": "^1.0.0-next.3",
|
"next-connect": "^1.0.0-next.3",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
@@ -64,28 +65,28 @@
|
|||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.4.2",
|
||||||
"@types/passport-local": "^1.0.35",
|
"@types/passport-local": "^1.0.35",
|
||||||
"@types/react-dom": "^18.2.7",
|
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
"@types/sparkpost": "^2.1.5",
|
"@types/sparkpost": "^2.1.5",
|
||||||
"@vercel/fetch": "^7.0.0",
|
"@vercel/fetch": "^7.0.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"daisyui": "^3.9.2",
|
"daisyui": "^3.9.2",
|
||||||
"dotenv-cli": "^7.2.1",
|
"dotenv-cli": "^7.2.1",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "17.1.0",
|
"eslint-config-airbnb-typescript": "17.1.0",
|
||||||
"eslint-config-next": "^13.5.4",
|
"eslint-config-next": "^13.5.4",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint": "^8.51.0",
|
|
||||||
"generate-password": "^1.7.1",
|
"generate-password": "^1.7.1",
|
||||||
"onchange": "^7.1.0",
|
"onchange": "^7.1.0",
|
||||||
"postcss": "^8.4.26",
|
"postcss": "^8.4.26",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-jsdoc": "^1.0.2",
|
"prettier-plugin-jsdoc": "^1.0.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
"prettier-plugin-tailwindcss": "^0.4.1",
|
||||||
"prettier": "^3.0.0",
|
|
||||||
"prisma": "^5.6.0",
|
"prisma": "^5.6.0",
|
||||||
"tailwindcss-animate": "^1.0.6",
|
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
|
"tailwindcss-animate": "^1.0.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.3.2"
|
"typescript": "^5.3.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import Image from 'next/image';
|
import { CldImage } from 'next-cloudinary';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
||||||
import { FaUser } from 'react-icons/fa';
|
import { FaUser } from 'react-icons/fa';
|
||||||
@@ -25,11 +25,12 @@ const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Image
|
<CldImage
|
||||||
src={userAvatar.path}
|
src={userAvatar.path}
|
||||||
alt="user avatar"
|
alt="user avatar"
|
||||||
width={1000}
|
width={1000}
|
||||||
height={1000}
|
height={1000}
|
||||||
|
crop="fill"
|
||||||
className="h-full w-full object-cover mask mask-circle"
|
className="h-full w-full object-cover mask mask-circle"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import Image from 'next/image';
|
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import UserContext from '@/contexts/UserContext';
|
import UserContext from '@/contexts/UserContext';
|
||||||
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
|
||||||
|
import { CldImage } from 'next-cloudinary';
|
||||||
import BeerPostLikeButton from '../BeerById/BeerPostLikeButton';
|
import BeerPostLikeButton from '../BeerById/BeerPostLikeButton';
|
||||||
|
|
||||||
const BeerCard: FC<{ post: z.infer<typeof BeerPostQueryResult> }> = ({ post }) => {
|
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">
|
<figure className="h-96">
|
||||||
<Link href={`/beers/${post.id}`} className="h-full object-cover">
|
<Link href={`/beers/${post.id}`} className="h-full object-cover">
|
||||||
{post.beerImages.length > 0 && (
|
{post.beerImages.length > 0 && (
|
||||||
<Image
|
<CldImage
|
||||||
src={post.beerImages[0].path}
|
src={post.beerImages[0].path}
|
||||||
alt={post.name}
|
alt={post.name}
|
||||||
|
crop="fill"
|
||||||
width="3000"
|
width="3000"
|
||||||
height="3000"
|
height="3000"
|
||||||
className="h-full object-cover"
|
className="h-full object-cover"
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import BreweryPostQueryResult from '@/services/BreweryPost/schema/BreweryPostQue
|
|||||||
import { FC, useContext } from 'react';
|
import { FC, useContext } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import Image from 'next/image';
|
|
||||||
|
import { CldImage } from 'next-cloudinary';
|
||||||
import BreweryPostLikeButton from './BreweryPostLikeButton';
|
import BreweryPostLikeButton from './BreweryPostLikeButton';
|
||||||
|
|
||||||
const BreweryCard: FC<{ brewery: z.infer<typeof BreweryPostQueryResult> }> = ({
|
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">
|
<figure className="card-image h-96">
|
||||||
<Link href={`/breweries/${brewery.id}`} className="h-full object-cover">
|
<Link href={`/breweries/${brewery.id}`} className="h-full object-cover">
|
||||||
{brewery.breweryImages.length > 0 && (
|
{brewery.breweryImages.length > 0 && (
|
||||||
<Image
|
<CldImage
|
||||||
src={brewery.breweryImages[0].path}
|
src={brewery.breweryImages[0].path}
|
||||||
alt={brewery.name}
|
alt={brewery.name}
|
||||||
width="1029"
|
width="1029"
|
||||||
height="110"
|
height="110"
|
||||||
|
crop="fill"
|
||||||
className="h-full object-cover"
|
className="h-full object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import sendUploadBeerImagesRequest from '@/requests/BeerImage/sendUploadBeerImag
|
|||||||
|
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
import Button from './ui/forms/Button';
|
import Button from './ui/forms/Button';
|
||||||
import FormError from './ui/forms/FormError';
|
import FormError from './ui/forms/FormError';
|
||||||
import FormInfo from './ui/forms/FormInfo';
|
import FormInfo from './ui/forms/FormInfo';
|
||||||
@@ -51,14 +52,14 @@ const CreateBeerPostForm: FunctionComponent<BeerFormProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const loadingToast = toast.loading('Creating beer post...');
|
||||||
const beerPost = await sendCreateBeerPostRequest(data);
|
const beerPost = await sendCreateBeerPostRequest(data);
|
||||||
await sendUploadBeerImagesRequest({ beerPost, images: data.images });
|
await sendUploadBeerImagesRequest({ beerPost, images: data.images });
|
||||||
await router.push(`/beers/${beerPost.id}`);
|
await router.push(`/beers/${beerPost.id}`);
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
toast.success('Created beer post.');
|
toast.success('Created beer post.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
createErrorToast(e);
|
||||||
|
|
||||||
toast.error(errorMessage);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import deleteBeerPostRequest from '@/requests/BeerPost/deleteBeerPostRequest';
|
import deleteBeerPostRequest from '@/requests/BeerPost/deleteBeerPostRequest';
|
||||||
import EditBeerPostValidationSchema from '@/services/BeerPost/schema/EditBeerPostValidationSchema';
|
import EditBeerPostValidationSchema from '@/services/BeerPost/schema/EditBeerPostValidationSchema';
|
||||||
import sendEditBeerPostRequest from '@/requests/BeerPost/sendEditBeerPostRequest';
|
import sendEditBeerPostRequest from '@/requests/BeerPost/sendEditBeerPostRequest';
|
||||||
|
import createErrorToast from '@/util/createErrorToast';
|
||||||
import Button from './ui/forms/Button';
|
import Button from './ui/forms/Button';
|
||||||
import FormError from './ui/forms/FormError';
|
import FormError from './ui/forms/FormError';
|
||||||
import FormInfo from './ui/forms/FormInfo';
|
import FormInfo from './ui/forms/FormInfo';
|
||||||
@@ -33,23 +34,26 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
|
|||||||
const { isSubmitting, errors } = formState;
|
const { isSubmitting, errors } = formState;
|
||||||
const onSubmit: SubmitHandler<EditBeerPostSchema> = async (data) => {
|
const onSubmit: SubmitHandler<EditBeerPostSchema> = async (data) => {
|
||||||
try {
|
try {
|
||||||
|
const loadingToast = toast.loading('Editing beer post...');
|
||||||
await sendEditBeerPostRequest(data);
|
await sendEditBeerPostRequest(data);
|
||||||
await router.push(`/beers/${data.id}`);
|
await router.push(`/beers/${data.id}`);
|
||||||
toast.success('Edited beer post.');
|
toast.success('Edited beer post.');
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
createErrorToast(e);
|
||||||
toast.error(errorMessage);
|
|
||||||
await router.push(`/beers/${data.id}`);
|
await router.push(`/beers/${data.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
try {
|
try {
|
||||||
|
const loadingToast = toast.loading('Deleting beer post...');
|
||||||
await deleteBeerPostRequest(previousValues.id);
|
await deleteBeerPostRequest(previousValues.id);
|
||||||
|
toast.dismiss(loadingToast);
|
||||||
await router.push('/beers');
|
await router.push('/beers');
|
||||||
|
toast.success('Deleted beer post.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Something went wrong.';
|
createErrorToast(e);
|
||||||
toast.error(errorMessage);
|
|
||||||
await router.push(`/beers`);
|
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 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, useContext } from 'react';
|
||||||
import { FC } from 'react';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import GetUserSchema from '@/services/User/schema/GetUserSchema';
|
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 UserAvatar from '../Account/UserAvatar';
|
||||||
|
import UserFollowButton from './UserFollowButton';
|
||||||
|
|
||||||
interface UserHeaderProps {
|
interface UserHeaderProps {
|
||||||
user: z.infer<typeof GetUserSchema>;
|
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 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 (
|
return (
|
||||||
<header className="card text-center items-center">
|
<header className="card text-center items-center">
|
||||||
<div className="card-body items-center w-full">
|
<div className="card-body items-center w-full">
|
||||||
@@ -42,6 +56,25 @@ const UserHeader: FC<UserHeaderProps> = ({ user, followerCount, followingCount }
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</header>
|
</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 { 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({
|
cloudinary.config({
|
||||||
cloud_name: CLOUDINARY_CLOUD_NAME,
|
cloud_name: NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,
|
||||||
api_key: CLOUDINARY_KEY,
|
api_key: CLOUDINARY_KEY,
|
||||||
api_secret: CLOUDINARY_SECRET,
|
api_secret: CLOUDINARY_SECRET,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error
|
/** Cloudinary storage instance. */
|
||||||
const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'BeerApp' } });
|
const storage = new CloudinaryStorage({ cloudinary, params: { folder: 'biergarten' } });
|
||||||
|
|
||||||
/** Configuration object for Cloudinary image upload. */
|
export { cloudinary, storage };
|
||||||
const cloudinaryConfig = { cloudinary, storage };
|
|
||||||
|
|
||||||
export default cloudinaryConfig;
|
|
||||||
|
|||||||
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({
|
const envSchema = z.object({
|
||||||
BASE_URL: z.string().url(),
|
BASE_URL: z.string().url(),
|
||||||
CLOUDINARY_CLOUD_NAME: z.string(),
|
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME: z.string(),
|
||||||
CLOUDINARY_KEY: z.string(),
|
CLOUDINARY_KEY: z.string(),
|
||||||
CLOUDINARY_SECRET: z.string(),
|
CLOUDINARY_SECRET: z.string(),
|
||||||
RESET_PASSWORD_TOKEN_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
|
* @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.
|
* 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 json = await response.json();
|
||||||
|
|
||||||
const count = response.headers.get('X-Total-Count');
|
const count = response.headers.get('X-Total-Count');
|
||||||
|
|
||||||
const parsed = APIResponseValidationSchema.safeParse(json);
|
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 };
|
return { following: parsedPayload.data, pageCount, followingCount: count };
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
const { data, error, isLoading, setSize, size, mutate } = useSWRInfinite(
|
||||||
(index) =>
|
(index) =>
|
||||||
`/api/users/${userId}/following?page_num=${index + 1}&page_size=${pageSize}`,
|
`/api/users/${userId}/following?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
fetcher,
|
fetcher,
|
||||||
@@ -57,6 +57,7 @@ const useGetUsersFollowedByUser = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
isLoadingMore,
|
isLoadingMore,
|
||||||
isAtEnd,
|
isAtEnd,
|
||||||
|
mutate,
|
||||||
error: error as unknown,
|
error: error as unknown,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const useGetUsersFollowingUser = ({
|
|||||||
return { followers: parsedPayload.data, pageCount, followerCount: count };
|
return { followers: parsedPayload.data, pageCount, followerCount: count };
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, error, isLoading, setSize, size } = useSWRInfinite(
|
const { data, error, isLoading, setSize, size, mutate } = useSWRInfinite(
|
||||||
(index) =>
|
(index) =>
|
||||||
`/api/users/${userId}/followers?page_num=${index + 1}&page_size=${pageSize}`,
|
`/api/users/${userId}/followers?page_num=${index + 1}&page_size=${pageSize}`,
|
||||||
fetcher,
|
fetcher,
|
||||||
@@ -57,6 +57,7 @@ const useGetUsersFollowingUser = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
isLoadingMore,
|
isLoadingMore,
|
||||||
isAtEnd,
|
isAtEnd,
|
||||||
|
mutate,
|
||||||
error: error as unknown,
|
error: error as unknown,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const AccountPage: NextPage = () => {
|
|||||||
<div className="flex flex-col items-center">
|
<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="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="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} />
|
<UserAvatar user={user} />
|
||||||
</div>
|
</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 NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
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 getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
|
||||||
import multer from 'multer';
|
|
||||||
|
|
||||||
import cloudinaryConfig from '@/config/cloudinary';
|
|
||||||
import { NextApiResponse } from 'next';
|
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 addBeerImageToDB from '@/services/BeerImage/addBeerImageToDB';
|
import addBeerImageToDB from '@/services/BeerImage/addBeerImageToDB';
|
||||||
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
||||||
|
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||||
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 UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
|
interface UploadBeerPostImagesRequest extends UserExtendedNextApiRequest {
|
||||||
files?: Express.Multer.File[];
|
files?: Express.Multer.File[];
|
||||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
|||||||
router.post(
|
router.post(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
uploadMiddleware,
|
uploadMiddlewareMultiple,
|
||||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||||
processImageData,
|
processImageData,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const editBeerPost = async (
|
|||||||
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
|
|
||||||
const deleted = deleteBeerPostById({ beerPostId: id });
|
const deleted = await deleteBeerPostById({ beerPostId: id });
|
||||||
if (!deleted) {
|
if (!deleted) {
|
||||||
throw new ServerError('Beer post not found', 404);
|
throw new ServerError('Beer post not found', 404);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ const getAllBeersByBrewery = async (
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
const { page_size, page_num, id } = req.query;
|
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>[] =
|
const beers: z.infer<typeof BeerPostQueryResult>[] =
|
||||||
await DBClient.instance.beerPost.findMany({
|
await DBClient.instance.beerPost.findMany({
|
||||||
where: { breweryId: id },
|
where: { breweryId: id },
|
||||||
take: parseInt(page_size, 10),
|
skip: (pageNum - 1) * pageSize,
|
||||||
skip: parseInt(page_num, 10) * parseInt(page_size, 10),
|
take: pageSize,
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
|||||||
@@ -1,38 +1,17 @@
|
|||||||
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
|
||||||
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
|
||||||
import { UserExtendedNextApiRequest } from '@/config/auth/types';
|
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 getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
|
||||||
|
|
||||||
import multer from 'multer';
|
|
||||||
|
|
||||||
import cloudinaryConfig from '@/config/cloudinary';
|
|
||||||
import { NextApiResponse } from 'next';
|
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 ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
import ImageMetadataValidationSchema from '@/services/schema/ImageSchema/ImageMetadataValidationSchema';
|
||||||
import addBreweryImageToDB from '@/services/BreweryImage/addBreweryImageToDB';
|
import addBreweryImageToDB from '@/services/BreweryImage/addBreweryImageToDB';
|
||||||
|
import { uploadMiddlewareMultiple } from '@/config/multer/uploadMiddleware';
|
||||||
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 {
|
interface UploadBreweryPostImagesRequest extends UserExtendedNextApiRequest {
|
||||||
files?: Express.Multer.File[];
|
files?: Express.Multer.File[];
|
||||||
@@ -75,7 +54,7 @@ const router = createRouter<
|
|||||||
router.post(
|
router.post(
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
uploadMiddleware,
|
uploadMiddlewareMultiple,
|
||||||
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
validateRequest({ bodySchema: ImageMetadataValidationSchema }),
|
||||||
processImageData,
|
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 { NextPage, GetServerSideProps } from 'next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
import getBeerPostById from '@/services/BeerPost/getBeerPostById';
|
||||||
|
|
||||||
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
import BeerPostQueryResult from '@/services/BeerPost/schema/BeerPostQueryResult';
|
||||||
@@ -13,6 +11,7 @@ import { Carousel } from 'react-responsive-carousel';
|
|||||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||||
import { Tab } from '@headlessui/react';
|
import { Tab } from '@headlessui/react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
import { CldImage } from 'next-cloudinary';
|
||||||
|
|
||||||
const [BeerInfoHeader, BeerPostCommentsSection, BeerRecommendations] = [
|
const [BeerInfoHeader, BeerPostCommentsSection, BeerRecommendations] = [
|
||||||
dynamic(() => import('@/components/BeerById/BeerInfoHeader')),
|
dynamic(() => import('@/components/BeerById/BeerInfoHeader')),
|
||||||
@@ -45,10 +44,11 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
|
|||||||
{beerPost.beerImages.length
|
{beerPost.beerImages.length
|
||||||
? beerPost.beerImages.map((image, index) => (
|
? beerPost.beerImages.map((image, index) => (
|
||||||
<div key={image.id} id={`image-${index}}`} className="w-full">
|
<div key={image.id} id={`image-${index}}`} className="w-full">
|
||||||
<Image
|
<CldImage
|
||||||
alt={image.alt}
|
alt={image.alt}
|
||||||
src={image.path}
|
src={image.path}
|
||||||
height={1080}
|
height={1080}
|
||||||
|
crop="fill"
|
||||||
width={1920}
|
width={1920}
|
||||||
className="h-96 w-full object-cover lg:h-[42rem]"
|
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GetServerSideProps, NextPage } from 'next';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
|
||||||
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
import 'react-responsive-carousel/lib/styles/carousel.min.css'; // requires a loader
|
||||||
import { Carousel } from 'react-responsive-carousel';
|
import { Carousel } from 'react-responsive-carousel';
|
||||||
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
|
||||||
@@ -12,6 +12,7 @@ import { Tab } from '@headlessui/react';
|
|||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
|
import { MAPBOX_ACCESS_TOKEN } from '@/config/env';
|
||||||
|
import { CldImage } from 'next-cloudinary';
|
||||||
|
|
||||||
const [BreweryInfoHeader, BreweryBeersSection, BreweryCommentsSection, BreweryPostMap] = [
|
const [BreweryInfoHeader, BreweryBeersSection, BreweryCommentsSection, BreweryPostMap] = [
|
||||||
dynamic(() => import('@/components/BreweryById/BreweryInfoHeader')),
|
dynamic(() => import('@/components/BreweryById/BreweryInfoHeader')),
|
||||||
@@ -47,9 +48,10 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost, mapboxToken
|
|||||||
{breweryPost.breweryImages.length
|
{breweryPost.breweryImages.length
|
||||||
? breweryPost.breweryImages.map((image, index) => (
|
? breweryPost.breweryImages.map((image, index) => (
|
||||||
<div key={image.id} id={`image-${index}}`} className="w-full">
|
<div key={image.id} id={`image-${index}}`} className="w-full">
|
||||||
<Image
|
<CldImage
|
||||||
alt={image.alt}
|
alt={image.alt}
|
||||||
src={image.path}
|
src={image.path}
|
||||||
|
crop="fill"
|
||||||
height={1080}
|
height={1080}
|
||||||
width={1920}
|
width={1920}
|
||||||
className="h-96 w-full object-cover lg:h-[42rem]"
|
className="h-96 w-full object-cover lg:h-[42rem]"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
|
|
||||||
import LoginForm from '@/components/Login/LoginForm';
|
import LoginForm from '@/components/Login/LoginForm';
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
import { FaUserCircle } from 'react-icons/fa';
|
import { FaUserCircle } from 'react-icons/fa';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
|
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
|
||||||
|
import { CldImage } from 'next-cloudinary';
|
||||||
|
|
||||||
const LoginPage: NextPage = () => {
|
const LoginPage: NextPage = () => {
|
||||||
useRedirectWhenLoggedIn();
|
useRedirectWhenLoggedIn();
|
||||||
@@ -20,11 +20,11 @@ const LoginPage: NextPage = () => {
|
|||||||
|
|
||||||
<div className="flex h-full flex-row">
|
<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%]">
|
<div className="hidden h-full flex-col items-center justify-center bg-base-100 lg:flex lg:w-[55%]">
|
||||||
<Image
|
<CldImage
|
||||||
src="https://picsum.photos/5000/5000"
|
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
|
||||||
alt="Login Image"
|
alt="Login Image"
|
||||||
width={4920}
|
width={5000}
|
||||||
height={4080}
|
height={5000}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { FC } from 'react';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
import withPageAuthRequired from '@/util/withPageAuthRequired';
|
||||||
import UserHeader from '@/components/UserPage/UserHeader';
|
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 {
|
interface UserInfoPageProps {
|
||||||
user: z.infer<typeof GetUserSchema>;
|
user: z.infer<typeof GetUserSchema>;
|
||||||
@@ -19,16 +17,6 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
|||||||
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
const isDesktop = useMediaQuery('(min-width: 1024px)');
|
||||||
const title = `${user.username} | The Biergarten App`;
|
const title = `${user.username} | The Biergarten App`;
|
||||||
|
|
||||||
const { followingCount } = useGetUsersFollowedByUser({
|
|
||||||
userId: user.id,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { followerCount } = useGetUsersFollowingUser({
|
|
||||||
userId: user.id,
|
|
||||||
pageSize: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -36,34 +24,9 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
|
|||||||
<meta name="description" content="User information" />
|
<meta name="description" content="User information" />
|
||||||
</Head>
|
</Head>
|
||||||
<>
|
<>
|
||||||
<main className="mb-12 mt-10 flex w-full items-center justify-center">
|
<main className="mb-12 mt-10 flex flex-col w-full items-center justify-center">
|
||||||
<div className="h-full w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
|
||||||
<UserHeader
|
<UserHeader user={user} />
|
||||||
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>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ const createNewUsers = async ({ numberOfUsers }: CreateNewUsersArgs) => {
|
|||||||
|
|
||||||
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
const dateOfBirth = faker.date.birthdate({ mode: 'age', min: 19 });
|
||||||
const createdAt = faker.date.past({ years: 4 });
|
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 = {
|
const user: UserData = {
|
||||||
firstName,
|
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