Merge pull request #67 from aaronpo97/dev

feat: implement user follow feature, remove multer-storage-cloudinary
This commit is contained in:
Aaron Po
2023-12-01 15:08:16 -05:00
committed by GitHub
34 changed files with 867 additions and 193 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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"
}, },

View File

@@ -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"
/> />
); );

View File

@@ -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"

View File

@@ -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"
/> />
)} )}

View File

@@ -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);
} }
}; };

View File

@@ -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`);
} }
}; };

View 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;

View File

@@ -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>
); );

View 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;

View File

@@ -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;

View File

@@ -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.

View 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'),
);

View File

@@ -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);

View 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;

View File

@@ -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,
}; };
}; };

View File

@@ -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,
}; };
}; };

View File

@@ -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>

View 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)) } };
});

View File

@@ -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,
); );

View File

@@ -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);
} }

View File

@@ -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,

View File

@@ -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,
); );

View 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;

View 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;

View 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 } };

View File

@@ -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]"
/> />

View File

@@ -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]"

View File

@@ -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>

View File

@@ -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>
</> </>

View File

@@ -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,

View 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;