Merge pull request #78 from aaronpo97/dev

Dev
This commit is contained in:
Aaron Po
2024-04-07 16:02:52 -04:00
committed by GitHub
125 changed files with 1616 additions and 1268 deletions

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ next-env.d.ts
/cloudinary-images
.obsidian

218
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"@react-email/tailwind": "^0.0.12",
"@vercel/analytics": "^1.1.0",
"argon2": "^0.31.1",
"classnames": "^2.5.1",
"cloudinary": "^1.41.0",
"cookie": "^0.5.0",
"date-fns": "^2.30.0",
@@ -37,7 +38,7 @@
"passport-local": "^1.0.0",
"pino": "^8.14.1",
"react": "^18.2.0",
"react-daisyui": "^4.1.2",
"react-daisyui": "^5.0.0",
"react-dom": "^18.2.0",
"react-email": "^1.9.5",
"react-hook-form": "^7.45.2",
@@ -62,10 +63,9 @@
"@types/passport-local": "^1.0.35",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/sparkpost": "^2.1.5",
"@vercel/fetch": "^7.0.0",
"autoprefixer": "^10.4.14",
"daisyui": "^3.9.2",
"daisyui": "^4.7.2",
"dotenv-cli": "^7.2.1",
"eslint": "^8.51.0",
"eslint-config-airbnb-base": "15.0.0",
@@ -80,8 +80,8 @@
"prettier-plugin-jsdoc": "^1.0.2",
"prettier-plugin-tailwindcss": "^0.5.7",
"prisma": "^5.7.0",
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.6",
"tailwindcss": "^3.4.1",
"tailwindcss-animated": "^1.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
}
@@ -2108,12 +2108,6 @@
"@types/responselike": "^1.0.0"
}
},
"node_modules/@types/caseless": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz",
"integrity": "sha512-ZD/NsIJYq/2RH+hY7lXmstfp/v9djGt9ah+xRQ3pcgR79qiKsG4pLl25AI7IcXxVO8dH9GiBE5rAknC0ePntlw==",
"dev": true
},
"node_modules/@types/connect": {
"version": "3.4.36",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
@@ -2375,32 +2369,6 @@
"@types/react": "*"
}
},
"node_modules/@types/request": {
"version": "2.48.9",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.9.tgz",
"integrity": "sha512-4mi2hYsvPAhe8RXjk5DKB09sAUzbK68T2XjORehHdWyxFoX2zUnfi1VQ5wU4Md28H/5+uB4DkxY9BS4B87N/0A==",
"dev": true,
"dependencies": {
"@types/caseless": "*",
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.0"
}
},
"node_modules/@types/request/node_modules/form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/@types/responselike": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@@ -2448,22 +2416,6 @@
"@types/node": "*"
}
},
"node_modules/@types/sparkpost": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@types/sparkpost/-/sparkpost-2.1.6.tgz",
"integrity": "sha512-lqimYaHi52iIJBge6XZBvLFGBjlZgSlsvUsARALgaYLpsUriPhayYKc8fyP5dgHmjJ6ClumP3BQHYT5Vbci6ew==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/request": "*"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz",
"integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==",
"dev": true
},
"node_modules/@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
@@ -3618,9 +3570,9 @@
}
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/cli-cursor": {
"version": "3.1.0",
@@ -3714,11 +3666,6 @@
"color-support": "bin.js"
}
},
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -3882,16 +3829,23 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/culori": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.9.4.tgz",
"integrity": "sha512-fvi2RGH4YV617/6DntOVGcOugOPym9jTGWW2XySb5ZpvdWO4L7bEG77VHirrnbRUEWvIEVXkBpxUz2KFj0rVnA==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.7.2.tgz",
"integrity": "sha512-9UCss12Zmyk/22u+JbkVrHHxOzFOyY17HuqP5LeswI4hclbj6qbjJTovdj2zRy8cCH6/n6Wh0lTLjriGnyGh0g==",
"dependencies": {
"colord": "^2.9",
"css-selector-tokenizer": "^0.8",
"postcss": "^8",
"postcss-js": "^4",
"tailwindcss": "^3.1"
"culori": "^3",
"picocolors": "^1",
"postcss-js": "^4"
},
"engines": {
"node": ">=16.9.0"
@@ -8694,11 +8648,11 @@
}
},
"node_modules/react-daisyui": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-daisyui/-/react-daisyui-4.1.2.tgz",
"integrity": "sha512-Sx8ziaxKDe/59bw+UxTFOoDSJEuA8iGhgmMbzSAtnhaaZPP20kluHG+1/wY5mBSxfcAuk6oI8fqKcJRp55WzPQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-daisyui/-/react-daisyui-5.0.0.tgz",
"integrity": "sha512-j1cugAxALmIbihycGBh7P9H3UXrfsvqu84qM6O55l7nOKST43IjUkZjmNav/7s0ZaUpa9Y+52mFchUb2zvon1A==",
"peerDependencies": {
"daisyui": "^3.0.22",
"daisyui": "^4.4.6",
"react": ">=16",
"react-dom": ">=16",
"tailwindcss": ">=3.2.7"
@@ -9969,19 +9923,19 @@
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
},
"node_modules/tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -10004,13 +9958,13 @@
"node": ">=14.0.0"
}
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
"node_modules/tailwindcss-animated": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tailwindcss-animated/-/tailwindcss-animated-1.0.1.tgz",
"integrity": "sha512-u5wusj89ZwP8I+s8WZlaAd7aZTWBN/XEG6QgMKpkIKmAf3xP1A6WYf7oYIKmGaB10UAQaSqWopi/i1ozzZEs8Q==",
"dev": true,
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders"
"tailwindcss": ">=3.1.0"
}
},
"node_modules/tailwindcss/node_modules/arg": {
@@ -12221,12 +12175,6 @@
"@types/responselike": "^1.0.0"
}
},
"@types/caseless": {
"version": "0.12.3",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz",
"integrity": "sha512-ZD/NsIJYq/2RH+hY7lXmstfp/v9djGt9ah+xRQ3pcgR79qiKsG4pLl25AI7IcXxVO8dH9GiBE5rAknC0ePntlw==",
"dev": true
},
"@types/connect": {
"version": "3.4.36",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz",
@@ -12487,31 +12435,6 @@
"@types/react": "*"
}
},
"@types/request": {
"version": "2.48.9",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.9.tgz",
"integrity": "sha512-4mi2hYsvPAhe8RXjk5DKB09sAUzbK68T2XjORehHdWyxFoX2zUnfi1VQ5wU4Md28H/5+uB4DkxY9BS4B87N/0A==",
"dev": true,
"requires": {
"@types/caseless": "*",
"@types/node": "*",
"@types/tough-cookie": "*",
"form-data": "^2.5.0"
},
"dependencies": {
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
}
}
},
"@types/responselike": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.1.tgz",
@@ -12559,22 +12482,6 @@
"@types/node": "*"
}
},
"@types/sparkpost": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@types/sparkpost/-/sparkpost-2.1.6.tgz",
"integrity": "sha512-lqimYaHi52iIJBge6XZBvLFGBjlZgSlsvUsARALgaYLpsUriPhayYKc8fyP5dgHmjJ6ClumP3BQHYT5Vbci6ew==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/request": "*"
}
},
"@types/tough-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz",
"integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==",
"dev": true
},
"@types/unist": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
@@ -13368,9 +13275,9 @@
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
},
"classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"cli-cursor": {
"version": "3.1.0",
@@ -13438,11 +13345,6 @@
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -13573,16 +13475,20 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"culori": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz",
"integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ=="
},
"daisyui": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.9.4.tgz",
"integrity": "sha512-fvi2RGH4YV617/6DntOVGcOugOPym9jTGWW2XySb5ZpvdWO4L7bEG77VHirrnbRUEWvIEVXkBpxUz2KFj0rVnA==",
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.7.2.tgz",
"integrity": "sha512-9UCss12Zmyk/22u+JbkVrHHxOzFOyY17HuqP5LeswI4hclbj6qbjJTovdj2zRy8cCH6/n6Wh0lTLjriGnyGh0g==",
"requires": {
"colord": "^2.9",
"css-selector-tokenizer": "^0.8",
"postcss": "^8",
"postcss-js": "^4",
"tailwindcss": "^3.1"
"culori": "^3",
"picocolors": "^1",
"postcss-js": "^4"
}
},
"damerau-levenshtein": {
@@ -16918,9 +16824,9 @@
}
},
"react-daisyui": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-daisyui/-/react-daisyui-4.1.2.tgz",
"integrity": "sha512-Sx8ziaxKDe/59bw+UxTFOoDSJEuA8iGhgmMbzSAtnhaaZPP20kluHG+1/wY5mBSxfcAuk6oI8fqKcJRp55WzPQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-daisyui/-/react-daisyui-5.0.0.tgz",
"integrity": "sha512-j1cugAxALmIbihycGBh7P9H3UXrfsvqu84qM6O55l7nOKST43IjUkZjmNav/7s0ZaUpa9Y+52mFchUb2zvon1A==",
"requires": {}
},
"react-dom": {
@@ -17861,19 +17767,19 @@
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
},
"tailwindcss": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz",
"integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==",
"requires": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
"chokidar": "^3.5.3",
"didyoumean": "^1.2.2",
"dlv": "^1.1.3",
"fast-glob": "^3.2.12",
"fast-glob": "^3.3.0",
"glob-parent": "^6.0.2",
"is-glob": "^4.0.3",
"jiti": "^1.18.2",
"jiti": "^1.19.1",
"lilconfig": "^2.1.0",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
@@ -17896,10 +17802,10 @@
}
}
},
"tailwindcss-animate": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
"tailwindcss-animated": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tailwindcss-animated/-/tailwindcss-animated-1.0.1.tgz",
"integrity": "sha512-u5wusj89ZwP8I+s8WZlaAd7aZTWBN/XEG6QgMKpkIKmAf3xP1A6WYf7oYIKmGaB10UAQaSqWopi/i1ozzZEs8Q==",
"dev": true,
"requires": {}
},

View File

@@ -28,6 +28,7 @@
"@react-email/tailwind": "^0.0.12",
"@vercel/analytics": "^1.1.0",
"argon2": "^0.31.1",
"classnames": "^2.5.1",
"cloudinary": "^1.41.0",
"cookie": "^0.5.0",
"date-fns": "^2.30.0",
@@ -36,13 +37,14 @@
"lodash": "^4.17.21",
"mapbox-gl": "^2.15.0",
"multer": "^1.4.5-lts.1",
"next": "^14.0.3",
"next-cloudinary": "^5.10.0",
"next-connect": "^1.0.0-next.3",
"next": "^14.0.3",
"passport-local": "^1.0.0",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"pino": "^8.14.1",
"react-daisyui": "^4.1.2",
"react": "^18.2.0",
"react-daisyui": "^5.0.0",
"react-dom": "^18.2.0",
"react-email": "^1.9.5",
"react-hook-form": "^7.45.2",
@@ -51,7 +53,6 @@
"react-intersection-observer": "^9.5.2",
"react-map-gl": "^7.1.2",
"react-responsive-carousel": "^3.2.23",
"react": "^18.2.0",
"swr": "^2.2.0",
"theme-change": "^2.5.0",
"zod": "^3.21.4"
@@ -66,28 +67,27 @@
"@types/multer": "^1.4.7",
"@types/node": "^20.4.2",
"@types/passport-local": "^1.0.35",
"@types/react-dom": "^18.2.7",
"@types/react": "^18.2.15",
"@types/sparkpost": "^2.1.5",
"@types/react-dom": "^18.2.7",
"@vercel/fetch": "^7.0.0",
"autoprefixer": "^10.4.14",
"daisyui": "^3.9.2",
"daisyui": "^4.7.2",
"dotenv-cli": "^7.2.1",
"eslint": "^8.51.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-next": "^13.5.4",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-react": "^7.33.2",
"eslint": "^8.51.0",
"generate-password": "^1.7.1",
"onchange": "^7.1.0",
"postcss": "^8.4.26",
"prettier": "^3.0.0",
"prettier-plugin-jsdoc": "^1.0.2",
"prettier-plugin-tailwindcss": "^0.5.7",
"prettier": "^3.0.0",
"prisma": "^5.7.0",
"tailwindcss-animate": "^1.0.6",
"tailwindcss": "^3.3.3",
"tailwindcss": "^3.4.1",
"tailwindcss-animated": "^1.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.3.2"
},

BIN
public/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@@ -1,5 +1,4 @@
import validateEmailRequest from '@/requests/User/validateEmailRequest';
import validateUsernameRequest from '@/requests/validateUsernameRequest';
import validateUsernameRequest from '@/requests/users/profile/validateUsernameRequest';
import { BaseCreateUserSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import { Switch } from '@headlessui/react';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -7,7 +6,7 @@ import { Dispatch, FC, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import UserContext from '@/contexts/UserContext';
import sendEditUserRequest from '@/requests/User/sendEditUserRequest';
import createErrorToast from '@/util/createErrorToast';
import { toast } from 'react-hot-toast';
import { AccountPageAction, AccountPageState } from '@/reducers/accountPageReducer';
@@ -15,6 +14,7 @@ import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel';
import FormTextInput from '../ui/forms/FormTextInput';
import { sendEditUserRequest, validateEmailRequest } from '@/requests/users/auth';
interface AccountInfoProps {
pageState: AccountPageState;
@@ -36,7 +36,7 @@ const AccountInfo: FC<AccountInfoProps> = ({ pageState, dispatch }) => {
.refine(
async (email) => {
if (user!.email === email) return true;
return validateEmailRequest(email);
return validateEmailRequest({ email });
},
{ message: 'Email is already taken.' },
),

View File

@@ -4,10 +4,11 @@ import { SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { UpdatePasswordSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import sendUpdatePasswordRequest from '@/requests/User/sendUpdatePasswordRequest';
import { AccountPageState, AccountPageAction } from '@/reducers/accountPageReducer';
import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import { sendUpdatePasswordRequest } from '@/requests/users/auth';
import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel';

View File

@@ -5,7 +5,7 @@ import { FaArrowRight } from 'react-icons/fa';
const UpdateProfileLink: React.FC = () => {
return (
<div className="card mt-8">
<div className="card">
<div className="card-body flex flex-col space-y-3">
<div className="flex w-full items-center justify-between space-x-5">
<div className="">

View File

@@ -8,11 +8,9 @@ const UserPosts: FC = () => {
<div className="mt-4">
<div>
<Tab.Group>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab-xl tab w-1/2 uppercase ui-selected:tab-active">Beers</Tab>
<Tab className="tab-xl tab w-1/2 uppercase ui-selected:tab-active">
Breweries
</Tab>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Beers</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Breweries</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>

View File

@@ -1,5 +1,3 @@
import sendCreateBeerCommentRequest from '@/requests/BeerComment/sendCreateBeerCommentRequest';
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -11,7 +9,8 @@ import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPost
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import CommentForm from '../ui/CommentForm';
import { sendCreateBeerCommentRequest } from '@/requests/comments/beer-comment';
import CommentForm from '../Comments/CommentForm';
interface BeerCommentFormProps {
beerPost: z.infer<typeof BeerPostQueryResult>;
@@ -34,11 +33,7 @@ const BeerCommentForm: FunctionComponent<BeerCommentFormProps> = ({
) => {
const loadingToast = toast.loading('Posting a new comment...');
try {
await sendCreateBeerCommentRequest({
content: data.content,
rating: data.rating,
beerPostId: beerPost.id,
});
await sendCreateBeerCommentRequest({ body: data, beerPostId: beerPost.id });
reset();
toast.remove(loadingToast);
toast.success('Comment posted successfully.');

View File

@@ -6,11 +6,15 @@ import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod';
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
import { useRouter } from 'next/router';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import {
deleteBeerPostCommentRequest,
editBeerPostCommentRequest,
} from '@/requests/comments/beer-comment';
import BeerCommentForm from './BeerCommentForm';
import LoadingComponent from './LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent';
import CommentLoadingComponent from '../Comments/CommentLoadingComponent';
import CommentsComponent from '../Comments/CommentsComponent';
interface BeerPostCommentsSectionProps {
beerPost: z.infer<typeof BeerPostQueryResult>;
@@ -28,29 +32,6 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const handleDeleteRequest = async (id: string) => {
const response = await fetch(`/api/beer-comments/${id}`, { method: 'DELETE' });
if (!response.ok) {
throw new Error('Failed to delete comment.');
}
};
const handleEditRequest = async (
id: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => {
const response = await fetch(`/api/beer-comments/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: data.content, rating: data.rating }),
});
if (!response.ok) {
throw new Error('Failed to update comment.');
}
};
return (
<div className="w-full space-y-3" ref={commentSectionRef}>
<div className="card bg-base-300">
@@ -72,7 +53,7 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
*/
isLoading ? (
<div className="card bg-base-300 pb-6">
<LoadingComponent length={PAGE_SIZE} />
<CommentLoadingComponent length={PAGE_SIZE} />
</div>
) : (
<CommentsComponent
@@ -84,8 +65,19 @@ const BeerPostCommentsSection: FC<BeerPostCommentsSectionProps> = ({ beerPost })
setSize={setSize}
size={size}
mutate={mutate}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
handleDeleteCommentRequest={(id) => {
return deleteBeerPostCommentRequest({
commentId: id,
beerPostId: beerPost.id,
});
}}
handleEditCommentRequest={(id, data) => {
return editBeerPostCommentRequest({
body: data,
commentId: id,
beerPostId: beerPost.id,
});
}}
/>
)
}

View File

@@ -3,7 +3,7 @@ import useCheckIfUserLikesBeerPost from '@/hooks/data-fetching/beer-likes/useChe
import { FC, useEffect, useState } from 'react';
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
import sendBeerPostLikeRequest from '@/requests/BeerPostLike/sendBeerPostLikeRequest';
import sendBeerPostLikeRequest from '@/requests/likes/beer-post-like/sendBeerPostLikeRequest';
import LikeButton from '../ui/LikeButton';
const BeerPostLikeButton: FC<{

View File

@@ -1,10 +1,11 @@
import Link from 'next/link';
import { FC, MutableRefObject, useRef } from 'react';
import { FC } from 'react';
import { useInView } from 'react-intersection-observer';
import { z } from 'zod';
import useBeerRecommendations from '@/hooks/data-fetching/beer-posts/useBeerRecommendations';
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
import debounce from 'lodash/debounce';
import BeerRecommendationLoadingComponent from './BeerRecommendationLoadingComponent';
const BeerRecommendationsSection: FC<{
@@ -28,10 +29,8 @@ const BeerRecommendationsSection: FC<{
},
});
const beerRecommendationsRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
return (
<div className="card h-full" ref={beerRecommendationsRef}>
<div className="card h-full">
<div className="card-body">
<>
<div className="my-2 flex flex-row items-center justify-between">
@@ -54,6 +53,7 @@ const BeerRecommendationsSection: FC<{
<div
ref={isPenultimateBeerPost ? penultimateBeerPostRef : undefined}
key={post.id}
className="animate-fade"
>
<div className="flex flex-col">
<Link className="link-hover link" href={`/beers/${post.id}`}>

View File

@@ -10,8 +10,8 @@ import createErrorToast from '@/util/createErrorToast';
import BeerStyleQueryResult from '@/services/posts/beer-style-post/schema/BeerStyleQueryResult';
import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments';
import sendCreateBeerStyleCommentRequest from '@/requests/BeerStyleComment/sendCreateBeerStyleCommentRequest';
import CommentForm from '../ui/CommentForm';
import { sendCreateBeerStyleCommentRequest } from '@/requests/comments/beer-style-comment';
import CommentForm from '../Comments/CommentForm';
interface BeerCommentFormProps {
beerStyle: z.infer<typeof BeerStyleQueryResult>;
@@ -35,8 +35,7 @@ const BeerStyleCommentForm: FunctionComponent<BeerCommentFormProps> = ({
const loadingToast = toast.loading('Posting a new comment...');
try {
await sendCreateBeerStyleCommentRequest({
content: data.content,
rating: data.rating,
body: { content: data.content, rating: data.rating },
beerStyleId: beerStyle.id,
});
reset();

View File

@@ -3,12 +3,15 @@ import UserContext from '@/contexts/UserContext';
import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod';
import { useRouter } from 'next/router';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import BeerStyleQueryResult from '@/services/posts/beer-style-post/schema/BeerStyleQueryResult';
import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments';
import LoadingComponent from '../BeerById/LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent';
import {
sendDeleteBeerStyleCommentRequest,
sendEditBeerStyleCommentRequest,
} from '@/requests/comments/beer-style-comment';
import CommentLoadingComponent from '../Comments/CommentLoadingComponent';
import CommentsComponent from '../Comments/CommentsComponent';
import BeerStyleCommentForm from './BeerStyleCommentForm';
interface BeerStyleCommentsSectionProps {
@@ -27,31 +30,6 @@ const BeerStyleCommentsSection: FC<BeerStyleCommentsSectionProps> = ({ beerStyle
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const handleDeleteRequest = async (id: string) => {
const response = await fetch(`/api/beer-style-comments/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete comment.');
}
};
const handleEditRequest = async (
id: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => {
const response = await fetch(`/api/beer-style-comments/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: data.content, rating: data.rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
};
return (
<div className="w-full space-y-3" ref={commentSectionRef}>
<div className="card bg-base-300">
@@ -73,7 +51,7 @@ const BeerStyleCommentsSection: FC<BeerStyleCommentsSectionProps> = ({ beerStyle
*/
isLoading ? (
<div className="card bg-base-300 pb-6">
<LoadingComponent length={PAGE_SIZE} />
<CommentLoadingComponent length={PAGE_SIZE} />
</div>
) : (
<CommentsComponent
@@ -85,8 +63,19 @@ const BeerStyleCommentsSection: FC<BeerStyleCommentsSectionProps> = ({ beerStyle
setSize={setSize}
size={size}
mutate={mutate}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
handleDeleteCommentRequest={(id) => {
return sendDeleteBeerStyleCommentRequest({
beerStyleId: beerStyle.id,
commentId: id,
});
}}
handleEditCommentRequest={(id, data) => {
return sendEditBeerStyleCommentRequest({
beerStyleId: beerStyle.id,
commentId: id,
body: data,
});
}}
/>
)
}

View File

@@ -2,7 +2,7 @@ import { FC, useEffect, useState } from 'react';
import useGetBeerPostLikeCount from '@/hooks/data-fetching/beer-likes/useBeerPostLikeCount';
import useCheckIfUserLikesBeerStyle from '@/hooks/data-fetching/beer-style-likes/useCheckIfUserLikesBeerPost';
import sendBeerStyleLikeRequest from '@/requests/BeerStyleLike/sendBeerStyleLikeRequest';
import sendBeerStyleLikeRequest from '@/requests/likes/beer-style-like/sendBeerStyleLikeRequest';
import LikeButton from '../ui/LikeButton';
const BeerStyleLikeButton: FC<{

View File

@@ -6,9 +6,9 @@ import { FC } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import toast from 'react-hot-toast';
import { z } from 'zod';
import sendCreateBreweryCommentRequest from '@/requests/BreweryComment/sendCreateBreweryCommentRequest';
import sendCreateBreweryCommentRequest from '@/requests/comments/brewery-comment/sendCreateBreweryCommentRequest';
import createErrorToast from '@/util/createErrorToast';
import CommentForm from '../ui/CommentForm';
import CommentForm from '../Comments/CommentForm';
interface BreweryCommentFormProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;

View File

@@ -2,11 +2,14 @@ import UserContext from '@/contexts/UserContext';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import { FC, MutableRefObject, useContext, useRef } from 'react';
import { z } from 'zod';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
import LoadingComponent from '../BeerById/LoadingComponent';
import CommentsComponent from '../ui/CommentsComponent';
import {
sendDeleteBreweryPostCommentRequest,
sendEditBreweryPostCommentRequest,
} from '@/requests/comments/brewery-comment';
import CommentLoadingComponent from '../Comments/CommentLoadingComponent';
import CommentsComponent from '../Comments/CommentsComponent';
import BreweryCommentForm from './BreweryCommentForm';
interface BreweryBeerSectionProps {
@@ -30,31 +33,6 @@ const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) =>
const commentSectionRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const handleDeleteRequest = async (commentId: string) => {
const response = await fetch(`/api/brewery-comments/${commentId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(response.statusText);
}
};
const handleEditRequest = async (
commentId: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => {
const response = await fetch(`/api/brewery-comments/${commentId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: data.content, rating: data.rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
};
return (
<div className="w-full space-y-3" ref={commentSectionRef}>
<div className="card">
@@ -75,7 +53,7 @@ const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) =>
*/
isLoading ? (
<div className="card pb-6">
<LoadingComponent length={PAGE_SIZE} />
<CommentLoadingComponent length={PAGE_SIZE} />
</div>
) : (
<CommentsComponent
@@ -87,8 +65,19 @@ const BreweryCommentsSection: FC<BreweryBeerSectionProps> = ({ breweryPost }) =>
size={size}
commentSectionRef={commentSectionRef}
mutate={mutate}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
handleDeleteCommentRequest={(id) => {
return sendDeleteBreweryPostCommentRequest({
breweryPostId: breweryPost.id,
commentId: id,
});
}}
handleEditCommentRequest={(commentId, data) => {
return sendEditBreweryPostCommentRequest({
breweryPostId: breweryPost.id,
commentId,
body: { content: data.content, rating: data.rating },
});
}}
/>
)
}

View File

@@ -1,6 +1,6 @@
import useCheckIfUserLikesBreweryPost from '@/hooks/data-fetching/brewery-likes/useCheckIfUserLikesBreweryPost';
import useGetBreweryPostLikeCount from '@/hooks/data-fetching/brewery-likes/useGetBreweryPostLikeCount';
import sendBreweryPostLikeRequest from '@/requests/BreweryPostLike/sendBreweryPostLikeRequest';
import sendBreweryPostLikeRequest from '@/requests/likes/brewery-post-like/sendBreweryPostLikeRequest';
import { FC, useState } from 'react';
import LikeButton from '../ui/LikeButton';

View File

@@ -1,5 +1,5 @@
import sendUploadBreweryImagesRequest from '@/requests/BreweryImage/sendUploadBreweryImageRequest';
import sendCreateBreweryPostRequest from '@/requests/BreweryPost/sendCreateBreweryPostRequest';
import sendUploadBreweryImagesRequest from '@/requests/images/brewery-image/sendUploadBreweryImageRequest';
import CreateBreweryPostSchema from '@/services/posts/brewery-post/schema/CreateBreweryPostSchema';
import UploadImageValidationSchema from '@/services/schema/ImageSchema/UploadImageValidationSchema';
import createErrorToast from '@/util/createErrorToast';
@@ -27,6 +27,7 @@ import FormSegment from '../ui/forms/FormSegment';
import FormTextArea from '../ui/forms/FormTextArea';
import FormTextInput from '../ui/forms/FormTextInput';
import Button from '../ui/forms/Button';
import { sendCreateBreweryPostRequest } from '@/requests/posts/brewery-post';
const AddressAutofill = dynamic(
// @ts-expect-error
@@ -225,7 +226,7 @@ const CreateBreweryPostForm: FC<{
if (!(data.images instanceof FileList)) {
return;
}
const breweryPost = await sendCreateBreweryPostRequest(data);
const breweryPost = await sendCreateBreweryPostRequest({ body: data });
await sendUploadBreweryImagesRequest({ breweryPost, images: data.images });
await router.push(`/breweries/${breweryPost.id}`);
toast.remove(loadingToast);
@@ -249,13 +250,9 @@ const CreateBreweryPostForm: FC<{
autoComplete="off"
>
<Tab.Group as={Fragment}>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Information
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Location
</Tab>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Information</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Location</Tab>
</Tab.List>
<Tab.Panels className="mt-4">
<Tab.Panel>

View File

@@ -3,28 +3,26 @@ import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResu
import { FC, useState } from 'react';
import { useInView } from 'react-intersection-observer';
import { z } from 'zod';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import CommentContentBody from './CommentContentBody';
import EditCommentBody from './EditCommentBody';
import UserAvatar from '../Account/UserAvatar';
import { HandleDeleteCommentRequest, HandleEditCommentRequest } from './types';
interface CommentCardProps {
comment: z.infer<typeof CommentQueryResult>;
mutate: ReturnType<typeof useBeerPostComments>['mutate'];
ref?: ReturnType<typeof useInView>['ref'];
handleDeleteRequest: (id: string) => Promise<void>;
handleEditRequest: (
id: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => Promise<void>;
handleDeleteCommentRequest: HandleDeleteCommentRequest;
handleEditCommentRequest: HandleEditCommentRequest;
}
const CommentCardBody: FC<CommentCardProps> = ({
comment,
mutate,
ref,
handleDeleteRequest,
handleEditRequest,
handleDeleteCommentRequest,
handleEditCommentRequest,
}) => {
const [inEditMode, setInEditMode] = useState(false);
@@ -44,8 +42,8 @@ const CommentCardBody: FC<CommentCardProps> = ({
comment={comment}
mutate={mutate}
setInEditMode={setInEditMode}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
handleDeleteCommentRequest={handleDeleteCommentRequest}
handleEditCommentRequest={handleEditCommentRequest}
/>
)}
</div>

View File

@@ -7,6 +7,8 @@ import Link from 'next/link';
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
import { z } from 'zod';
import { useInView } from 'react-intersection-observer';
import classNames from 'classnames';
import CommentCardDropdown from './CommentCardDropdown';
interface CommentContentBodyProps {
@@ -17,9 +19,16 @@ interface CommentContentBodyProps {
const CommentContentBody: FC<CommentContentBodyProps> = ({ comment, setInEditMode }) => {
const { user } = useContext(UserContext);
const timeDistance = useTimeDistance(new Date(comment.createdAt));
const [ref, inView] = useInView({ triggerOnce: true });
return (
<div className="space-y-1 py-4 pr-3 animate-in fade-in-10">
<div
className={classNames('space-y-1 py-4 pr-3 fade-in-10', {
'opacity-0': !inView,
'animate-fade': inView,
})}
ref={ref}
>
<div className="space-y-2">
<div className="flex flex-row justify-between">
<div>

View File

@@ -8,12 +8,12 @@ import type {
UseFormSetValue,
UseFormWatch,
} from 'react-hook-form';
import FormError from './forms/FormError';
import FormInfo from './forms/FormInfo';
import FormLabel from './forms/FormLabel';
import FormSegment from './forms/FormSegment';
import FormTextArea from './forms/FormTextArea';
import Button from './forms/Button';
import FormError from '../ui/forms/FormError';
import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel';
import FormSegment from '../ui/forms/FormSegment';
import FormTextArea from '../ui/forms/FormTextArea';
import Button from '../ui/forms/Button';
interface Comment {
content: string;

View File

@@ -1,6 +1,6 @@
const CommentLoadingCardBody = () => {
return (
<div className="animate card-body h-52 fade-in-10">
<div className="card-body h-52 fade-in-10">
<div className="flex animate-pulse space-x-4 slide-in-from-top">
<div className="flex-1 space-y-4 py-1">
<div className="h-4 w-3/4 rounded bg-base-100" />

View File

@@ -1,12 +1,12 @@
import { FC } from 'react';
import Spinner from '../ui/Spinner';
import CommentLoadingCardBody from '../BeerBreweryComments/CommentLoadingCardBody';
import CommentLoadingCardBody from './CommentLoadingCardBody';
interface LoadingComponentProps {
interface CommentLoadingComponentProps {
length: number;
}
const LoadingComponent: FC<LoadingComponentProps> = ({ length }) => {
const CommentLoadingComponent: FC<CommentLoadingComponentProps> = ({ length }) => {
return (
<>
{Array.from({ length }).map((_, i) => (
@@ -19,4 +19,4 @@ const LoadingComponent: FC<LoadingComponentProps> = ({ length }) => {
);
};
export default LoadingComponent;
export default CommentLoadingComponent;

View File

@@ -6,53 +6,46 @@ import { useInView } from 'react-intersection-observer';
import useBeerPostComments from '@/hooks/data-fetching/beer-comments/useBeerPostComments';
import useBreweryPostComments from '@/hooks/data-fetching/brewery-comments/useBreweryPostComments';
import NoCommentsCard from '../BeerById/NoCommentsCard';
import LoadingComponent from '../BeerById/LoadingComponent';
import CommentCardBody from '../BeerBreweryComments/CommentCardBody';
import useBeerStyleComments from '@/hooks/data-fetching/beer-style-comments/useBeerStyleComments';
import NoCommentsCard from './NoCommentsCard';
import CommentLoadingComponent from './CommentLoadingComponent';
import CommentCardBody from './CommentCardBody';
import { HandleDeleteCommentRequest, HandleEditCommentRequest } from './types';
type HookReturnType = ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments | typeof useBeerStyleComments
>;
interface CommentsComponentProps {
comments: HookReturnType['comments'];
isAtEnd: HookReturnType['isAtEnd'];
isLoadingMore: HookReturnType['isLoadingMore'];
mutate: HookReturnType['mutate'];
setSize: HookReturnType['setSize'];
size: HookReturnType['size'];
commentSectionRef: MutableRefObject<HTMLDivElement | null>;
handleDeleteCommentRequest: HandleDeleteCommentRequest;
handleEditCommentRequest: HandleEditCommentRequest;
pageSize: number;
size: ReturnType<typeof useBeerPostComments | typeof useBreweryPostComments>['size'];
setSize: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['setSize'];
comments: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['comments'];
isAtEnd: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['isAtEnd'];
isLoadingMore: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['isLoadingMore'];
mutate: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['mutate'];
handleDeleteRequest: (id: string) => Promise<void>;
handleEditRequest: (
id: string,
data: { content: string; rating: number },
) => Promise<void>;
}
const CommentsComponent: FC<CommentsComponentProps> = ({
commentSectionRef,
comments,
commentSectionRef,
handleDeleteCommentRequest,
handleEditCommentRequest,
isAtEnd,
isLoadingMore,
mutate,
pageSize,
setSize,
size,
mutate,
handleDeleteRequest,
handleEditRequest,
}) => {
const { ref: penultimateCommentRef } = useInView({
threshold: 0.1,
/**
* When the last comment comes into view, call setSize from useBeerPostComments to
* load more comments.
* When the last comment comes into view, call setSize from the comment fetching hook
* to load more comments.
*/
onChange: (visible) => {
if (!visible || isAtEnd) return;
@@ -79,8 +72,8 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
<CommentCardBody
comment={comment}
mutate={mutate}
handleDeleteRequest={handleDeleteRequest}
handleEditRequest={handleEditRequest}
handleDeleteCommentRequest={handleDeleteCommentRequest}
handleEditCommentRequest={handleEditCommentRequest}
/>
</div>
);
@@ -91,7 +84,7 @@ const CommentsComponent: FC<CommentsComponentProps> = ({
* If there are more comments to load, show a loading component with a
* skeleton loader and a loading spinner.
*/
!!isLoadingMore && <LoadingComponent length={pageSize} />
!!isLoadingMore && <CommentLoadingComponent length={pageSize} />
}
{

View File

@@ -14,6 +14,9 @@ import FormInfo from '../ui/forms/FormInfo';
import FormLabel from '../ui/forms/FormLabel';
import FormSegment from '../ui/forms/FormSegment';
import FormTextArea from '../ui/forms/FormTextArea';
import { HandleDeleteCommentRequest, HandleEditCommentRequest } from './types';
import { useInView } from 'react-intersection-observer';
import classNames from 'classnames';
interface EditCommentBodyProps {
comment: z.infer<typeof CommentQueryResult>;
@@ -22,19 +25,16 @@ interface EditCommentBodyProps {
mutate: ReturnType<
typeof useBeerPostComments | typeof useBreweryPostComments
>['mutate'];
handleDeleteRequest: (id: string) => Promise<void>;
handleEditRequest: (
id: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => Promise<void>;
handleDeleteCommentRequest: HandleDeleteCommentRequest;
handleEditCommentRequest: HandleEditCommentRequest;
}
const EditCommentBody: FC<EditCommentBodyProps> = ({
comment,
setInEditMode,
mutate,
handleDeleteRequest,
handleEditRequest,
handleDeleteCommentRequest,
handleEditCommentRequest,
}) => {
const { register, handleSubmit, formState, setValue, watch } = useForm<
z.infer<typeof CreateCommentValidationSchema>
@@ -51,7 +51,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
const loadingToast = toast.loading('Deleting comment...');
setIsDeleting(true);
try {
await handleDeleteRequest(comment.id);
await handleDeleteCommentRequest(comment.id);
await mutate();
toast.remove(loadingToast);
toast.success('Deleted comment.');
@@ -68,7 +68,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
try {
setInEditMode(true);
await handleEditRequest(comment.id, data);
await handleEditCommentRequest(comment.id, data);
await mutate();
toast.remove(loadingToast);
toast.success('Comment edits submitted successfully.');
@@ -80,8 +80,18 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
}
};
const disableForm = isSubmitting || isDeleting;
const [ref, inView] = useInView({ triggerOnce: true });
return (
<div className="py-4 pr-3 animate-in fade-in-10">
<div
className={classNames('py-4 pr-3 animate-in fade-in-10', {
'opacity-0': !inView,
'animate-fade': inView,
})}
ref={ref}
>
<form onSubmit={handleSubmit(onEdit)} className="space-y-3">
<div>
<FormInfo>
@@ -95,7 +105,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
placeholder="Comment"
rows={2}
error={!!errors.content?.message}
disabled={isSubmitting || isDeleting}
disabled={disableForm}
/>
</FormSegment>
<div className="flex flex-row items-center justify-between">
@@ -114,8 +124,8 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
<Rating.Item
name="rating-1"
className="mask mask-star cursor-default"
disabled={isSubmitting || isDeleting}
aria-disabled={isSubmitting || isDeleting}
disabled={disableForm}
aria-disabled={disableForm}
key={index}
/>
))}
@@ -125,7 +135,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
<button
type="button"
className="btn join-item btn-xs lg:btn-sm"
disabled={isSubmitting || isDeleting}
disabled={disableForm}
onClick={() => {
setInEditMode(false);
}}
@@ -134,7 +144,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
</button>
<button
type="submit"
disabled={isSubmitting || isDeleting}
disabled={disableForm}
className="btn join-item btn-xs lg:btn-sm"
>
Save
@@ -143,7 +153,7 @@ const EditCommentBody: FC<EditCommentBodyProps> = ({
type="button"
className="btn join-item btn-xs lg:btn-sm"
onClick={onDelete}
disabled={isDeleting || formState.isSubmitting}
disabled={disableForm}
>
Delete
</button>

View File

@@ -0,0 +1,12 @@
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
type APIResponse = z.infer<typeof APIResponseValidationSchema>;
export type HandleEditCommentRequest = (
id: string,
data: z.infer<typeof CreateCommentValidationSchema>,
) => Promise<APIResponse>;
export type HandleDeleteCommentRequest = (id: string) => Promise<APIResponse>;

View File

@@ -1,18 +1,20 @@
import { FunctionComponent } from 'react';
import router from 'next/router';
import { zodResolver } from '@hookform/resolvers/zod';
import { BeerStyle } from '@prisma/client';
import router from 'next/router';
import { FunctionComponent } from 'react';
import toast from 'react-hot-toast';
import { useForm, SubmitHandler, FieldError } from 'react-hook-form';
import { z } from 'zod';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import CreateBeerPostValidationSchema from '@/services/posts/beer-post/schema/CreateBeerPostValidationSchema';
import sendCreateBeerPostRequest from '@/requests/BeerPost/sendCreateBeerPostRequest';
import UploadImageValidationSchema from '@/services/schema/ImageSchema/UploadImageValidationSchema';
import sendUploadBeerImagesRequest from '@/requests/BeerImage/sendUploadBeerImageRequest';
import toast from 'react-hot-toast';
import createErrorToast from '@/util/createErrorToast';
import { sendCreateBeerPostRequest } from '@/requests/posts/beer-post';
import sendUploadBeerImagesRequest from '@/requests/images/beer-image/sendUploadBeerImageRequest';
import Button from './ui/forms/Button';
import FormError from './ui/forms/FormError';
import FormInfo from './ui/forms/FormInfo';
@@ -53,7 +55,16 @@ const CreateBeerPostForm: FunctionComponent<BeerFormProps> = ({
try {
const loadingToast = toast.loading('Creating beer post...');
const beerPost = await sendCreateBeerPostRequest(data);
const beerPost = await sendCreateBeerPostRequest({
body: {
name: data.name,
description: data.description,
abv: data.abv,
ibu: data.ibu,
},
breweryId: data.breweryId,
styleId: data.styleId,
});
await sendUploadBeerImagesRequest({ beerPost, images: data.images });
await router.push(`/beers/${beerPost.id}`);
toast.dismiss(loadingToast);

View File

@@ -6,10 +6,13 @@ import { z } from 'zod';
import { useForm, SubmitHandler } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import deleteBeerPostRequest from '@/requests/BeerPost/deleteBeerPostRequest';
import EditBeerPostValidationSchema from '@/services/posts/beer-post/schema/EditBeerPostValidationSchema';
import sendEditBeerPostRequest from '@/requests/BeerPost/sendEditBeerPostRequest';
import createErrorToast from '@/util/createErrorToast';
import {
sendEditBeerPostRequest,
sendDeleteBeerPostRequest,
} from '@/requests/posts/beer-post';
import Button from './ui/forms/Button';
import FormError from './ui/forms/FormError';
import FormInfo from './ui/forms/FormInfo';
@@ -35,7 +38,15 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
const onSubmit: SubmitHandler<EditBeerPostSchema> = async (data) => {
try {
const loadingToast = toast.loading('Editing beer post...');
await sendEditBeerPostRequest(data);
await sendEditBeerPostRequest({
beerPostId: data.id,
body: {
name: data.name,
abv: data.abv,
ibu: data.ibu,
description: data.description,
},
});
await router.push(`/beers/${data.id}`);
toast.success('Edited beer post.');
toast.dismiss(loadingToast);
@@ -48,7 +59,7 @@ const EditBeerPostForm: FC<EditBeerPostFormProps> = ({ previousValues }) => {
const onDelete = async () => {
try {
const loadingToast = toast.loading('Deleting beer post...');
await deleteBeerPostRequest(previousValues.id);
await sendDeleteBeerPostRequest({ beerPostId: previousValues.id });
toast.dismiss(loadingToast);
await router.push('/beers');
toast.success('Deleted beer post.');

View File

@@ -0,0 +1,106 @@
import EditBreweryPostValidationSchema from '@/services/posts/brewery-post/schema/EditBreweryPostValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { FC, useState } from 'react';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import {
sendDeleteBreweryPostRequest,
sendEditBreweryPostRequest,
} from '@/requests/posts/brewery-post';
import FormError from './ui/forms/FormError';
import FormInfo from './ui/forms/FormInfo';
import FormLabel from './ui/forms/FormLabel';
import FormSegment from './ui/forms/FormSegment';
import FormTextArea from './ui/forms/FormTextArea';
import FormTextInput from './ui/forms/FormTextInput';
interface EditBreweryPostFormProps {
breweryPost: z.infer<typeof BreweryPostQueryResult>;
}
const EditBreweryPostForm: FC<EditBreweryPostFormProps> = ({ breweryPost }) => {
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<z.infer<typeof EditBreweryPostValidationSchema>>({
resolver: zodResolver(EditBreweryPostValidationSchema),
defaultValues: {
name: breweryPost.name,
description: breweryPost.description,
id: breweryPost.id,
dateEstablished: breweryPost.dateEstablished,
},
});
const [isDeleting, setIsDeleting] = useState(false);
const onSubmit = async (data: z.infer<typeof EditBreweryPostValidationSchema>) => {
await sendEditBreweryPostRequest({ breweryPostId: breweryPost.id, body: data });
await router.push(`/breweries/${breweryPost.id}`);
};
const handleDelete = async () => {
setIsDeleting(true);
await sendDeleteBreweryPostRequest({ breweryPostId: breweryPost.id });
await router.push('/breweries');
};
return (
<form className="form-control space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div className="w-full">
<FormInfo>
<FormLabel htmlFor="name">Name</FormLabel>
<FormError>{errors.name?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextInput
id="name"
type="text"
placeholder="Name"
formValidationSchema={register('name')}
error={!!errors.name}
disabled={isSubmitting || isDeleting}
/>
</FormSegment>
<FormInfo>
<FormLabel htmlFor="description">Description</FormLabel>
<FormError>{errors.description?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextArea
disabled={isSubmitting || isDeleting}
placeholder="Ratione cumque quas quia aut impedit ea culpa facere. Ut in sit et quas reiciendis itaque."
error={!!errors.description}
formValidationSchema={register('description')}
id="description"
rows={8}
/>
</FormSegment>
</div>
<div className="w-full space-y-3">
<button
disabled={isSubmitting || isDeleting}
className="btn btn-primary w-full"
type="submit"
>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
<button
className="btn btn-primary w-full"
type="button"
disabled={isSubmitting || isDeleting}
onClick={handleDelete}
>
Delete Brewery
</button>
</div>
</form>
);
};
export default EditBreweryPostForm;

View File

@@ -1,4 +1,3 @@
import sendLoginUserRequest from '@/requests/User/sendLoginUserRequest';
import LoginValidationSchema from '@/services/users/auth/schema/LoginValidationSchema';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
@@ -15,6 +14,7 @@ import FormLabel from '../ui/forms/FormLabel';
import FormSegment from '../ui/forms/FormSegment';
import FormTextInput from '../ui/forms/FormTextInput';
import Button from '../ui/forms/Button';
import { sendLoginUserRequest } from '@/requests/users/auth';
type LoginT = z.infer<typeof LoginValidationSchema>;
const LoginForm = () => {
@@ -47,7 +47,7 @@ const LoginForm = () => {
};
return (
<form className="form-control w-full space-y-5" onSubmit={handleSubmit(onSubmit)}>
<form className="form-control space-y-5" onSubmit={handleSubmit(onSubmit)}>
<div>
<FormInfo>
<FormLabel htmlFor="username">username</FormLabel>

View File

@@ -1,4 +1,3 @@
import sendRegisterUserRequest from '@/requests/User/sendRegisterUserRequest';
import { CreateUserValidationSchemaWithUsernameAndEmailCheck } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/router';
@@ -9,6 +8,7 @@ import { z } from 'zod';
import createErrorToast from '@/util/createErrorToast';
import toast from 'react-hot-toast';
import { sendRegisterUserRequest } from '@/requests/users/auth';
import Button from './ui/forms/Button';
import FormError from './ui/forms/FormError';
import FormInfo from './ui/forms/FormInfo';
@@ -44,13 +44,9 @@ const RegisterUserForm: FC = () => {
}
};
return (
<form
className="form-control w-full space-y-5"
noValidate
onSubmit={handleSubmit(onSubmit)}
>
<div>
<div className="flex flex-col lg:flex-row lg:space-x-3">
<form className="form-control w-full" noValidate onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-5">
<div className="flex flex-col lg:flex-row lg:space-x-5">
<div className="lg:w-[50%]">
<FormInfo>
<FormLabel htmlFor="firstName">First name</FormLabel>
@@ -63,7 +59,7 @@ const RegisterUserForm: FC = () => {
type="text"
formValidationSchema={register('firstName')}
error={!!errors.firstName}
placeholder="first name"
placeholder="John"
/>
</FormSegment>
</div>
@@ -80,13 +76,13 @@ const RegisterUserForm: FC = () => {
type="text"
formValidationSchema={register('lastName')}
error={!!errors.lastName}
placeholder="last name"
placeholder="Doe"
/>
</FormSegment>
</div>
</div>
<div className="flex flex-col lg:flex-row lg:space-x-3">
<div className="flex flex-col lg:flex-row lg:space-x-5">
<div className="lg:w-[50%]">
<FormInfo>
<FormLabel htmlFor="email">email</FormLabel>
@@ -99,7 +95,7 @@ const RegisterUserForm: FC = () => {
type="email"
formValidationSchema={register('email')}
error={!!errors.email}
placeholder="email"
placeholder="john.doe@example.com"
/>
</FormSegment>
</div>
@@ -115,13 +111,13 @@ const RegisterUserForm: FC = () => {
type="text"
formValidationSchema={register('username')}
error={!!errors.username}
placeholder="username"
placeholder="johndoe"
/>
</FormSegment>
</div>
</div>
<div className="flex flex-col lg:flex-row lg:space-x-3">
<div className="flex flex-col lg:flex-row lg:space-x-5">
<div className="lg:w-[50%]">
<FormInfo>
<FormLabel htmlFor="password">password</FormLabel>
@@ -155,6 +151,7 @@ const RegisterUserForm: FC = () => {
</FormSegment>
</div>
</div>
<div>
<FormInfo>
<FormLabel htmlFor="dateOfBirth">Date of birth</FormLabel>
<FormError>{errors.dateOfBirth?.message}</FormError>
@@ -169,12 +166,13 @@ const RegisterUserForm: FC = () => {
placeholder="date of birth"
/>
</FormSegment>
<div className="mt-6 w-full">
</div>
</div>
<div className="mt-10">
<Button type="submit" isSubmitting={formState.isSubmitting}>
Register User
</Button>
</div>
</div>
</form>
);
};

View File

@@ -1,7 +1,8 @@
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 { sendUserFollowRequest } from '@/requests/users/auth';
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
import { FC, useState } from 'react';
import { FaUserCheck, FaUserPlus } from 'react-icons/fa';
@@ -25,7 +26,7 @@ const UserFollowButton: FC<UserFollowButtonProps> = ({
const onClick = async () => {
try {
setIsLoading(true);
await sendUserFollowRequest(user.id);
await sendUserFollowRequest({ userId: user.id });
await Promise.all([
mutateFollowStatus(),
mutateFollowerCount(),

View File

@@ -3,13 +3,10 @@ import Navbar from './Navbar';
const Layout: FC<{ children: ReactNode }> = ({ children }) => {
return (
<div className="flex h-screen flex-col">
<div className="flex h-full flex-col" id="app">
<Navbar />
<div className="top-0 h-full w-screen flex-1 overflow-x-auto animate-in fade-in">
{children}
</div>
</div>
);
};
export default Layout;

View File

@@ -1,11 +1,13 @@
import useMediaQuery from '@/hooks/utilities/useMediaQuery';
import useNavbar from '@/hooks/utilities/useNavbar';
import useTheme from '@/hooks/utilities/useTheme';
// import useTheme from '@/hooks/utilities/useTheme';
import Link from 'next/link';
import { FC } from 'react';
import { MdDarkMode, MdLightMode } from 'react-icons/md';
import { GiHamburgerMenu } from 'react-icons/gi';
import { FC, useRef } from 'react';
// import { MdDarkMode, MdLightMode } from 'react-icons/md';
import { FaBars } from 'react-icons/fa';
import classNames from 'classnames';
const DesktopLinks: FC = () => {
const { pages, currentURL } = useNavbar();
@@ -19,8 +21,8 @@ const DesktopLinks: FC = () => {
<Link tabIndex={0} href={page.slug} className="hover:bg-primary-focus">
<span
className={`text-lg uppercase ${
currentURL === page.slug ? 'font-black' : 'font-medium'
} text-primary-content`}
currentURL === page.slug ? 'font-extrabold' : 'font-bold'
} text-base-content`}
>
{page.name}
</span>
@@ -35,43 +37,76 @@ const DesktopLinks: FC = () => {
const MobileLinks: FC = () => {
const { pages } = useNavbar();
const drawerRef = useRef<HTMLInputElement>(null);
return (
<div className="flex-none lg:hidden">
<div className="dropdown-end dropdown">
<label tabIndex={0} className="btn btn-circle btn-ghost">
<GiHamburgerMenu />
<div className="drawer drawer-end">
<input id="my-drawer" type="checkbox" className="drawer-toggle" ref={drawerRef} />
<div className="drawer-content">
<label htmlFor="my-drawer" className="btn btn-ghost drawer-button">
<FaBars />
</label>
<ul
tabIndex={0}
className="menu-compact menu dropdown-content rounded-box mt-3 w-48 bg-base-100 p-2 shadow"
>
{pages.map((page) => (
</div>
<div className="drawer-side">
<label
htmlFor="my-drawer"
aria-label="close sidebar"
className="drawer-overlay"
/>
<ul className="menu min-h-full bg-primary pr-16 text-base-content">
{pages.map((page) => {
return (
<li key={page.slug}>
<Link href={page.slug}>
<span className="select-none text-primary-content">{page.name}</span>
<Link
href={page.slug}
tabIndex={0}
rel={page.slug === '/resume/main.pdf' ? 'noopener noreferrer' : ''}
target={page.slug === '/resume/main.pdf' ? '_blank' : ''}
onClick={() => {
if (!drawerRef.current) return;
drawerRef.current.checked = false;
}}
>
<span className="text-lg font-bold uppercase">{page.name}</span>
</Link>
</li>
))}
);
})}
</ul>
</div>
</div>
</div>
);
};
const Navbar = () => {
const isDesktopView = useMediaQuery('(min-width: 1024px)');
const { theme, setTheme } = useTheme();
const { currentURL } = useNavbar();
const backgroundIsTransparent =
currentURL === '/' || currentURL === '/login' || currentURL === '/register';
const isOnHomePage = currentURL === '/';
// const { theme, setTheme } = useTheme();
return (
<div className="navbar sticky top-0 z-50 bg-primary text-primary-content">
<div
className={classNames('navbar fixed top-0 z-20 h-10 min-h-10 text-base-content', {
'bg-transparent': backgroundIsTransparent,
'bg-base-100': !backgroundIsTransparent,
})}
>
<div className="flex-1">
<Link className="btn btn-ghost normal-case" href="/">
{isOnHomePage ? null : (
<Link className="btn btn-ghost btn-sm" href="/">
<span className="cursor-pointer text-lg font-bold">The Biergarten App</span>
</Link>
)}
</div>
<div
{/* <div
className="tooltip tooltip-left"
data-tip={theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
>
@@ -96,7 +131,7 @@ const Navbar = () => {
</button>
)}
</div>
</div>
</div> */}
<div>{isDesktopView ? <DesktopLinks /> : <MobileLinks />}</div>
</div>
);

View File

@@ -24,9 +24,9 @@ export const checkIfBeerCommentOwner = async <T extends CommentRequest>(
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
next: NextHandler,
) => {
const { id } = req.query;
const { commentId } = req.query;
const user = req.user!;
const comment = await getBeerPostCommentByIdService({ beerPostCommentId: id });
const comment = await getBeerPostCommentByIdService({ beerPostCommentId: commentId });
if (!comment) {
throw new ServerError('Comment not found', 404);
@@ -43,9 +43,9 @@ export const editBeerPostComment = async (
req: EditAndCreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { commentId } = req.query;
await editBeerPostCommentByIdService({ body: req.body, beerPostCommentId: id });
await editBeerPostCommentByIdService({ body: req.body, beerPostCommentId: commentId });
res.status(200).json({
success: true,
@@ -58,9 +58,9 @@ export const deleteBeerPostComment = async (
req: CommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { commentId } = req.query;
await deleteBeerCommentByIdService({ beerPostCommentId: id });
await deleteBeerCommentByIdService({ beerPostCommentId: commentId });
res.status(200).json({
success: true,
@@ -73,7 +73,7 @@ export const createBeerPostComment = async (
req: EditAndCreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const beerPostId = req.query.id;
const beerPostId = req.query.postId;
const newBeerComment = await createBeerPostCommentService({
body: req.body,
@@ -93,7 +93,7 @@ export const getAllBeerPostComments = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const beerPostId = req.query.id;
const beerPostId = req.query.postId;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;

View File

@@ -25,10 +25,12 @@ export const checkIfBeerStyleCommentOwner = async <
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
next: NextHandler,
) => {
const { id } = req.query;
const { commentId } = req.query;
const user = req.user!;
const beerStyleComment = await findBeerStyleCommentById({ beerStyleCommentId: id });
const beerStyleComment = await findBeerStyleCommentById({
beerStyleCommentId: commentId,
});
if (!beerStyleComment) {
throw new ServerError('Beer style comment not found.', 404);
@@ -49,7 +51,7 @@ export const editBeerStyleComment = async (
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
await updateBeerStyleCommentById({
beerStyleCommentId: req.query.id,
beerStyleCommentId: req.query.commentId,
body: req.body,
});
@@ -64,9 +66,9 @@ export const deleteBeerStyleComment = async (
req: CommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { commentId } = req.query;
await deleteBeerStyleCommentById({ beerStyleCommentId: id });
await deleteBeerStyleCommentById({ beerStyleCommentId: commentId });
res.status(200).json({
success: true,
@@ -81,7 +83,7 @@ export const createComment = async (
) => {
const newBeerStyleComment = await createNewBeerStyleComment({
body: req.body,
beerStyleId: req.query.id,
beerStyleId: req.query.postId,
userId: req.user!.id,
});
@@ -97,7 +99,7 @@ export const getAll = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const beerStyleId = req.query.id;
const beerStyleId = req.query.postId;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;

View File

@@ -25,9 +25,9 @@ export const checkIfBreweryCommentOwner = async <
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
next: NextHandler,
) => {
const { id } = req.query;
const { commentId } = req.query;
const user = req.user!;
const comment = await getBreweryCommentById({ breweryCommentId: id });
const comment = await getBreweryCommentById({ breweryCommentId: commentId });
if (!comment) {
throw new ServerError('Comment not found', 404);
@@ -44,10 +44,10 @@ export const editBreweryPostComment = async (
req: EditAndCreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { commentId } = req.query;
const updated = updateBreweryCommentById({
breweryCommentId: id,
const updated = await updateBreweryCommentById({
breweryCommentId: commentId,
body: req.body,
});
@@ -63,9 +63,9 @@ export const deleteBreweryPostComment = async (
req: CommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { commentId } = req.query;
await deleteBreweryCommentByIdService({ breweryCommentId: id });
await deleteBreweryCommentByIdService({ breweryCommentId: commentId });
res.status(200).json({
success: true,
@@ -78,7 +78,7 @@ export const createComment = async (
req: EditAndCreateCommentRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const breweryPostId = req.query.id;
const breweryPostId = req.query.postId;
const user = req.user!;
@@ -100,7 +100,7 @@ export const getAll = async (
req: GetAllCommentsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const breweryPostId = req.query.id;
const breweryPostId = req.query.postId;
// eslint-disable-next-line @typescript-eslint/naming-convention
const { page_size, page_num } = req.query;

View File

@@ -3,7 +3,7 @@ import CreateCommentValidationSchema from '@/services/schema/CommentSchema/Creat
import { z } from 'zod';
export interface CommentRequest extends UserExtendedNextApiRequest {
query: { id: string };
query: { postId: string; commentId: string };
}
export interface EditAndCreateCommentRequest extends CommentRequest {
@@ -11,5 +11,5 @@ export interface EditAndCreateCommentRequest extends CommentRequest {
}
export interface GetAllCommentsRequest extends UserExtendedNextApiRequest {
query: { id: string; page_size: string; page_num: string };
query: { postId: string; page_size: string; page_num: string };
}

View File

@@ -20,7 +20,7 @@ export const processBeerImageData = async (
}
const beerImages = await addBeerImagesService({
beerPostId: req.query.id,
beerPostId: req.query.postId,
userId: user!.id,
body,
files,

View File

@@ -18,7 +18,7 @@ export const processBreweryImageData = async (
}
const breweryImages = await addBreweryImagesService({
breweryPostId: req.query.id,
breweryPostId: req.query.postId,
userId: user!.id,
body,
files,

View File

@@ -4,7 +4,7 @@ import { z } from 'zod';
export interface UploadImagesRequest extends UserExtendedNextApiRequest {
files?: Express.Multer.File[];
query: { id: string };
query: { postId: string };
body: z.infer<typeof ImageMetadataValidationSchema>;
}

View File

@@ -2,7 +2,7 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types';
import ServerError from '@/config/util/ServerError';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse, NextApiRequest } from 'next';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import { getBeerPostById } from '@/services/posts/beer-post';
@@ -19,9 +19,9 @@ export const sendBeerPostLikeRequest = async (
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const id = req.query.id as string;
const { postId } = req.query;
const beer = await getBeerPostById({ beerPostId: id });
const beer = await getBeerPostById({ beerPostId: postId });
if (!beer) {
throw new ServerError('Could not find a beer post with that id.', 404);
}
@@ -53,12 +53,12 @@ export const sendBeerPostLikeRequest = async (
};
export const getBeerPostLikeCount = async (
req: NextApiRequest,
req: LikeRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id as string;
const { postId } = req.query;
const likeCount = await getBeerPostLikeCountService({ beerPostId: id });
const likeCount = await getBeerPostLikeCountService({ beerPostId: postId });
res.status(200).json({
success: true,

View File

@@ -1,7 +1,7 @@
import ServerError from '@/config/util/ServerError';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse, NextApiRequest } from 'next';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import {
@@ -18,9 +18,9 @@ export const sendBeerStyleLikeRequest = async (
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const { id } = req.query;
const { postId } = req.query;
const beerStyle = await getBeerStyleByIdService({ beerStyleId: id });
const beerStyle = await getBeerStyleByIdService({ beerStyleId: postId });
if (!beerStyle) {
throw new ServerError('Could not find a beer style with that id.', 404);
}
@@ -48,11 +48,11 @@ export const sendBeerStyleLikeRequest = async (
};
export const getBeerStyleLikeCountRequest = async (
req: NextApiRequest,
req: LikeRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id as string;
const likeCount = await getBeerStyleLikeCountService({ beerStyleId: id });
const { postId } = req.query;
const likeCount = await getBeerStyleLikeCountService({ beerStyleId: postId });
res.status(200).json({
success: true,

View File

@@ -1,4 +1,3 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
import ServerError from '@/config/util/ServerError';
import {
@@ -9,17 +8,18 @@ import {
} from '@/services/likes/brewery-post-like';
import { getBreweryPostByIdService } from '@/services/posts/brewery-post';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse, NextApiRequest } from 'next';
import { NextApiResponse } from 'next';
import { z } from 'zod';
import { LikeRequest } from '../types';
export const sendBreweryPostLikeRequest = async (
req: UserExtendedNextApiRequest,
req: LikeRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id! as string;
const { postId } = req.query;
const user = req.user!;
const breweryPost = await getBreweryPostByIdService({ breweryPostId: id });
const breweryPost = await getBreweryPostByIdService({ breweryPostId: postId });
if (!breweryPost) {
throw new ServerError('Could not find a brewery post with that id', 404);
@@ -53,12 +53,12 @@ export const sendBreweryPostLikeRequest = async (
};
export const getBreweryPostLikeCount = async (
req: NextApiRequest,
req: LikeRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const id = req.query.id! as string;
const { postId } = req.query;
const breweryPost = await getBreweryPostByIdService({ breweryPostId: id });
const breweryPost = await getBreweryPostByIdService({ breweryPostId: postId });
if (!breweryPost) {
throw new ServerError('Could not find a brewery post with that id', 404);
}
@@ -76,14 +76,14 @@ export const getBreweryPostLikeCount = async (
};
export const getBreweryPostLikeStatus = async (
req: UserExtendedNextApiRequest,
req: LikeRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const user = req.user!;
const id = req.query.id as string;
const { postId } = req.query;
const liked = await findBreweryPostLikeService({
breweryPostId: id,
breweryPostId: postId,
likedById: user.id,
});

View File

@@ -1,5 +1,5 @@
import { UserExtendedNextApiRequest } from '@/config/auth/types';
export interface LikeRequest extends UserExtendedNextApiRequest {
query: { id: string };
query: { postId: string };
}

View File

@@ -28,9 +28,9 @@ export const checkIfBeerPostOwner = async <BeerPostRequestType extends BeerPostR
next: NextHandler,
) => {
const { user, query } = req;
const { id } = query;
const { postId } = query;
const beerPost = await getBeerPostById({ beerPostId: id });
const beerPost = await getBeerPostById({ beerPostId: postId });
if (!beerPost) {
throw new ServerError('Beer post not found', 404);
@@ -47,7 +47,7 @@ export const editBeerPost = async (
req: EditBeerPostRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
await editBeerPostByIdService({ beerPostId: req.query.id, body: req.body });
await editBeerPostByIdService({ beerPostId: req.query.postId, body: req.body });
res.status(200).json({
message: 'Beer post updated successfully',
@@ -57,9 +57,9 @@ export const editBeerPost = async (
};
export const deleteBeerPost = async (req: BeerPostRequest, res: NextApiResponse) => {
const { id } = req.query;
const { postId } = req.query;
const deleted = await deleteBeerPostByIdService({ beerPostId: id });
const deleted = await deleteBeerPostByIdService({ beerPostId: postId });
if (!deleted) {
throw new ServerError('Beer post not found', 404);
}
@@ -75,9 +75,9 @@ export const getBeerPostRecommendations = async (
req: GetBeerRecommendationsRequest,
res: NextApiResponse<z.infer<typeof APIResponseValidationSchema>>,
) => {
const { id } = req.query;
const { postId } = req.query;
const beerPost = await getBeerPostById({ beerPostId: id });
const beerPost = await getBeerPostById({ beerPostId: postId });
if (!beerPost) {
throw new ServerError('Beer post not found', 404);

View File

@@ -5,7 +5,7 @@ import { NextApiRequest } from 'next';
import { z } from 'zod';
export interface BeerPostRequest extends UserExtendedNextApiRequest {
query: { id: string };
query: { postId: string };
}
export interface EditBeerPostRequest extends BeerPostRequest {
@@ -17,7 +17,7 @@ export interface GetAllBeerPostsRequest extends NextApiRequest {
}
export interface GetBeerRecommendationsRequest extends BeerPostRequest {
query: { id: string; page_num: string; page_size: string };
query: { postId: string; page_num: string; page_size: string };
}
export interface CreateBeerPostRequest extends UserExtendedNextApiRequest {

View File

@@ -167,9 +167,9 @@ export const checkIfBreweryPostOwner = async (
next: NextHandler,
) => {
const user = req.user!;
const { id } = req.query;
const { postId } = req.query;
const breweryPost = await getBreweryPostByIdService({ breweryPostId: id });
const breweryPost = await getBreweryPostByIdService({ breweryPostId: postId });
if (!breweryPost) {
throw new ServerError('Brewery post not found', 404);
}
@@ -187,10 +187,10 @@ export const editBreweryPost = async (
) => {
const {
body,
query: { id },
query: { postId },
} = req;
await updateBreweryPostService({ breweryPostId: id, body });
await updateBreweryPostService({ breweryPostId: postId, body });
res.status(200).json({
message: 'Brewery post updated successfully',
@@ -203,8 +203,8 @@ export const deleteBreweryPost = async (
req: BreweryPostRequest,
res: NextApiResponse,
) => {
const { id } = req.query;
const deleted = await deleteBreweryPostService({ breweryPostId: id });
const { postId } = req.query;
const deleted = await deleteBreweryPostService({ breweryPostId: postId });
if (!deleted) {
throw new ServerError('Brewery post not found', 404);

View File

@@ -14,7 +14,7 @@ export interface CreateBreweryPostRequest extends UserExtendedNextApiRequest {
}
export interface BreweryPostRequest extends UserExtendedNextApiRequest {
query: { id: string };
query: { postId: string };
}
export interface EditBreweryPostRequest extends BreweryPostRequest {

View File

@@ -41,7 +41,7 @@ const useGetBreweryPostLikeCount = (breweryPostId: string) => {
error: error as unknown,
isLoading,
mutate,
likeCount: data as number | undefined,
likeCount: data,
};
};

View File

@@ -8,7 +8,7 @@ const NotFound: NextPage = () => {
<title>404 Page Not Found</title>
<meta name="description" content="404 Page Not Found" />
</Head>
<div className="mx-2 flex h-full flex-col items-center justify-center text-center lg:mx-0">
<div className="mx-2 flex h-dvh flex-col items-center justify-center text-center lg:mx-0">
<h1 className="text-3xl font-bold lg:text-5xl">404: Not Found</h1>
<h2 className="text-lg font-bold">
Sorry, the page you are looking for does not exist.

View File

@@ -22,14 +22,16 @@ const router = createRouter<
router
.delete(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBeerCommentOwner,
deleteBeerPostComment,
)
.put(
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,

View File

@@ -11,6 +11,7 @@ import {
createBeerPostComment,
getAllBeerPostComments,
} from '@/controllers/comments/beer-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
const router = createRouter<
// @TODO: Fix this any type
@@ -21,7 +22,7 @@ const router = createRouter<
router.post(
validateRequest({
bodySchema: CreateCommentValidationSchema,
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createBeerPostComment,
@@ -29,11 +30,7 @@ router.post(
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeerPostComments,
);

View File

@@ -26,14 +26,14 @@ router
.put(
validateRequest({
bodySchema: EditBeerPostValidationSchema,
querySchema: z.object({ id: z.string() }),
querySchema: z.object({ postId: z.string() }),
}),
getCurrentUser,
checkIfBeerPostOwner,
editBeerPost,
)
.delete(
validateRequest({ querySchema: z.object({ id: z.string() }) }),
validateRequest({ querySchema: z.object({ postId: z.string() }) }),
getCurrentUser,
checkIfBeerPostOwner,
deleteBeerPost,

View File

@@ -19,12 +19,12 @@ const router = createRouter<
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBeerPostLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerPostLikeCount,
);

View File

@@ -16,7 +16,7 @@ const router = createRouter<
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
checkIfBeerPostIsLiked,
);

View File

@@ -2,6 +2,7 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getBeerPostRecommendations } from '@/controllers/posts/beer-posts';
import { GetBeerRecommendationsRequest } from '@/controllers/posts/beer-posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
@@ -14,11 +15,7 @@ const router = createRouter<
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_num: z.string().regex(/^[0-9]+$/),
page_size: z.string().regex(/^[0-9]+$/),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getBeerPostRecommendations,
);

View File

@@ -1,6 +1,7 @@
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getAllBeersByBeerStyle } from '@/controllers/posts/beer-styles';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiRequest, NextApiResponse } from 'next';
@@ -18,11 +19,7 @@ const router = createRouter<
router.get(
validateRequest({
querySchema: z.object({
page_size: z.string().min(1),
page_num: z.string().min(1),
id: z.string().min(1),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeersByBeerStyle,
);

View File

@@ -22,7 +22,7 @@ const router = createRouter<
router
.delete(
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBeerStyleCommentOwner,
@@ -30,7 +30,7 @@ router
)
.put(
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid(), commentId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,

View File

@@ -8,6 +8,7 @@ import getCurrentUser from '@/config/nextConnect/middleware/getCurrentUser';
import { NextApiResponse } from 'next';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import { createComment, getAll } from '@/controllers/comments/beer-style-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
const router = createRouter<
// I don't want to use any, but I can't figure out how to get the types to work
@@ -18,7 +19,7 @@ const router = createRouter<
router.post(
validateRequest({
bodySchema: CreateCommentValidationSchema,
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createComment,
@@ -26,11 +27,7 @@ router.post(
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAll,
);

View File

@@ -13,7 +13,7 @@ const router = createRouter<
>();
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerStyle,
);

View File

@@ -20,12 +20,12 @@ const router = createRouter<
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBeerStyleLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBeerStyleLikeCountRequest,
);

View File

@@ -15,7 +15,7 @@ const router = createRouter<
router.get(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
checkIfBeerStyleIsLiked,
);

View File

@@ -2,6 +2,7 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getAllBeersByBrewery } from '@/controllers/posts/breweries';
import { GetAllPostsByConnectedPostId } from '@/controllers/posts/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
import { createRouter } from 'next-connect';
@@ -14,11 +15,7 @@ const router = createRouter<
router.get(
validateRequest({
querySchema: z.object({
page_size: z.string().nonempty(),
page_num: z.string().nonempty(),
id: z.string().nonempty(),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAllBeersByBrewery,
);

View File

@@ -24,7 +24,7 @@ const router = createRouter<
router
.delete(
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ commentId: z.string().cuid(), postId: z.string().cuid() }),
}),
getCurrentUser,
checkIfBreweryCommentOwner,
@@ -32,7 +32,7 @@ router
)
.put(
validateRequest({
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ commentId: z.string().cuid(), postId: z.string().cuid() }),
bodySchema: CreateCommentValidationSchema,
}),
getCurrentUser,

View File

@@ -9,6 +9,7 @@ import { NextApiResponse } from 'next';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import { createComment, getAll } from '@/controllers/comments/brewery-comments';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
const router = createRouter<
// I don't want to use any, but I can't figure out how to get the types to work
@@ -19,7 +20,7 @@ const router = createRouter<
router.post(
validateRequest({
bodySchema: CreateCommentValidationSchema,
querySchema: z.object({ id: z.string().cuid() }),
querySchema: z.object({ postId: z.string().cuid() }),
}),
getCurrentUser,
createComment,
@@ -27,11 +28,7 @@ router.post(
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_size: z.coerce.number().int().positive(),
page_num: z.coerce.number().int().positive(),
}),
querySchema: PaginatedQueryResponseSchema.extend({ postId: z.string().cuid() }),
}),
getAll,
);

View File

@@ -1,4 +1,3 @@
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';
@@ -6,6 +5,7 @@ import {
sendBreweryPostLikeRequest,
getBreweryPostLikeCount,
} from '@/controllers/likes/brewery-post-likes';
import { LikeRequest } from '@/controllers/likes/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
@@ -13,18 +13,18 @@ import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
UserExtendedNextApiRequest,
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.post(
getCurrentUser,
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
sendBreweryPostLikeRequest,
);
router.get(
validateRequest({ querySchema: z.object({ id: z.string().cuid() }) }),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBreweryPostLikeCount,
);

View File

@@ -1,8 +1,8 @@
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 { getBreweryPostLikeStatus } from '@/controllers/likes/brewery-post-likes';
import { LikeRequest } from '@/controllers/likes/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
@@ -10,17 +10,13 @@ import { createRouter } from 'next-connect';
import { z } from 'zod';
const router = createRouter<
UserExtendedNextApiRequest,
LikeRequest,
NextApiResponse<z.infer<typeof APIResponseValidationSchema>>
>();
router.get(
getCurrentUser,
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
}),
}),
validateRequest({ querySchema: z.object({ postId: z.string().cuid() }) }),
getBreweryPostLikeStatus,
);

View File

@@ -2,6 +2,7 @@ import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getUserFollowers } from '@/controllers/users/profile';
import { GetUserFollowInfoRequest } from '@/controllers/users/profile/types';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
@@ -15,11 +16,7 @@ const router = createRouter<
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_size: z.string().regex(/^\d+$/),
page_num: z.string().regex(/^\d+$/),
}),
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getUserFollowers,
);

View File

@@ -2,6 +2,7 @@ import { UserExtendedNextApiRequest } from '@/config/auth/types';
import NextConnectOptions from '@/config/nextConnect/NextConnectOptions';
import validateRequest from '@/config/nextConnect/middleware/validateRequest';
import { getUsersFollowed } from '@/controllers/users/profile';
import PaginatedQueryResponseSchema from '@/services/schema/PaginatedQueryResponseSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { NextApiResponse } from 'next';
@@ -19,11 +20,7 @@ const router = createRouter<
router.get(
validateRequest({
querySchema: z.object({
id: z.string().cuid(),
page_size: z.string().regex(/^\d+$/),
page_num: z.string().regex(/^\d+$/),
}),
querySchema: PaginatedQueryResponseSchema.extend({ id: z.string().cuid() }),
}),
getUsersFollowed,
);

View File

@@ -74,13 +74,9 @@ const BeerByIdPage: NextPage<BeerPageProps> = ({ beerPost }) => {
</div>
) : (
<Tab.Group>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Comments
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Other Beers
</Tab>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Comments</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Other Beers</Tab>
</Tab.List>
<Tab.Panels className="mt-2">
<Tab.Panel>

View File

@@ -41,11 +41,9 @@ const BeerStyleByIdPage: NextPage<BeerStylePageProps> = ({ beerStyle }) => {
</div>
) : (
<Tab.Group>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Comments
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Comments</Tab>
<Tab className="tab uppercase ui-selected:tab-active">
Beers in this Style
</Tab>
</Tab.List>

View File

@@ -1,20 +1,11 @@
import FormError from '@/components/ui/forms/FormError';
import FormInfo from '@/components/ui/forms/FormInfo';
import FormLabel from '@/components/ui/forms/FormLabel';
import EditBreweryPostForm from '@/components/EditBreweryPostForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import FormSegment from '@/components/ui/forms/FormSegment';
import FormTextArea from '@/components/ui/forms/FormTextArea';
import FormTextInput from '@/components/ui/forms/FormTextInput';
import { getBreweryPostByIdService } from '@/services/posts/brewery-post';
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import EditBreweryPostValidationSchema from '@/services/posts/brewery-post/schema/EditBreweryPostValidationSchema';
import withPageAuthRequired from '@/util/withPageAuthRequired';
import { zodResolver } from '@hookform/resolvers/zod';
import { NextPage } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { BiBeer } from 'react-icons/bi';
import { z } from 'zod';
@@ -25,49 +16,6 @@ interface EditPageProps {
const EditBreweryPostPage: NextPage<EditPageProps> = ({ breweryPost }) => {
const pageTitle = `Edit \u201c${breweryPost.name}\u201d`;
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<z.infer<typeof EditBreweryPostValidationSchema>>({
resolver: zodResolver(EditBreweryPostValidationSchema),
defaultValues: {
name: breweryPost.name,
description: breweryPost.description,
id: breweryPost.id,
dateEstablished: breweryPost.dateEstablished,
},
});
const onSubmit = async (data: z.infer<typeof EditBreweryPostValidationSchema>) => {
const response = await fetch(`/api/breweries/${breweryPost.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Something went wrong');
}
router.push(`/breweries/${breweryPost.id}`);
};
const handleDelete = async () => {
const response = await fetch(`/api/breweries/${breweryPost.id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Something went wrong');
}
router.push('/breweries');
};
return (
<>
<Head>
@@ -81,59 +29,7 @@ const EditBreweryPostPage: NextPage<EditPageProps> = ({ breweryPost }) => {
backLink={`/breweries/${breweryPost.id}`}
backLinkText={`Back to "${breweryPost.name}"`}
>
<>
<form className="form-control space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div className="w-full">
<FormInfo>
<FormLabel htmlFor="name">Name</FormLabel>
<FormError>{errors.name?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextInput
id="name"
type="text"
placeholder="Name"
formValidationSchema={register('name')}
error={!!errors.name}
disabled={isSubmitting}
/>
</FormSegment>
<FormInfo>
<FormLabel htmlFor="description">Description</FormLabel>
<FormError>{errors.description?.message}</FormError>
</FormInfo>
<FormSegment>
<FormTextArea
disabled={isSubmitting}
placeholder="Ratione cumque quas quia aut impedit ea culpa facere. Ut in sit et quas reiciendis itaque."
error={!!errors.description}
formValidationSchema={register('description')}
id="description"
rows={8}
/>
</FormSegment>
</div>
<div className="w-full space-y-3">
<button
disabled={isSubmitting}
className="btn btn-primary w-full"
type="submit"
>
{isSubmitting ? 'Saving...' : 'Save'}
</button>
<button
className="btn btn-primary w-full"
type="button"
onClick={handleDelete}
>
Delete Brewery
</button>
</div>
</form>
</>
<EditBreweryPostForm breweryPost={breweryPost} />
</FormPageLayout>
</>
);

View File

@@ -85,11 +85,11 @@ const BreweryByIdPage: NextPage<BreweryPageProps> = ({ breweryPost, mapboxToken
token={mapboxToken}
/>
<Tab.Group>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab-md tab w-1/2 uppercase ui-selected:tab-active">
Comments
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
<Tab className="tab-md tab w-1/2 uppercase ui-selected:tab-active">
Beers
</Tab>
</Tab.List>

View File

@@ -1,4 +1,5 @@
import { NextPage } from 'next';
import { CldImage } from 'next-cloudinary';
import Head from 'next/head';
const keywords = [
@@ -29,7 +30,7 @@ const keywords = [
'beer recipes',
];
const description = `The Biergarten App is an app for beer lovers to share their favourite brews and breweries with like-minded people online.`;
const description = `An app for beer lovers to share their favourite brews and breweries with like-minded people online.`;
const Home: NextPage = () => {
return (
@@ -39,13 +40,17 @@ const Home: NextPage = () => {
<meta name="description" content={description} />
<meta name="keywords" content={keywords.join(', ')} />
</Head>
<div className="flex h-full w-full items-center justify-center bg-primary">
<div className="w-9/12 text-center lg:w-8/12">
<h1 className="text-3xl font-bold md:text-4xl lg:text-5xl xl:text-8xl">
The Biergarten App
</h1>
<p className="mt-4 text-lg lg:text-2xl">{description}</p>
<div className="relative flex h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-9/12 flex-col space-y-3 text-base-content">
<h1 className="text-5xl font-extrabold lg:text-8xl">The Biergarten App</h1>
<p className="font-bold lg:text-3xl">{description}</p>
</div>
</div>
</>

View File

@@ -18,37 +18,35 @@ const LoginPage: NextPage = () => {
<meta name="description" content="Login to your account" />
</Head>
<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="relative flex h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="h-full w-full object-cover"
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-9/12 flex-col items-center text-center text-base-content lg:w-6/12">
<div className="mb-8 flex w-full flex-col items-center space-y-2 text-center">
<FaUserCircle className="text-6xl text-primary-content" />
<h1 className="text-6xl font-extrabold">Login</h1>
</div>
<div className="flex h-full w-full flex-col items-center justify-center bg-base-300 lg:w-[45%]">
<div className="w-10/12 space-y-5 sm:w-9/12">
<div className=" flex flex-col items-center space-y-2">
<FaUserCircle className="text-3xl" />
<h1 className="text-4xl font-bold">Login</h1>
</div>
<div className="w-full">
<LoginForm />
<div className="mt-3 flex flex-col items-center space-y-1">
<Link href="/register" className="text-primary-500 link-hover link italic">
</div>
<div className="mt-5 flex w-full flex-col space-y-1 text-center font-bold italic">
<Link className="link-hover link text-lg" href="/register">
Don&apos;t have an account?
</Link>
<Link
href="/users/forgot-password"
className="text-primary-500 link-hover link italic"
>
Forgot password?
<Link className="link-hover link text-lg" href="/users/forgot-password">
Forgot your password?{' '}
</Link>
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,10 +1,11 @@
import RegisterUserForm from '@/components/RegisterUserForm';
import FormPageLayout from '@/components/ui/forms/FormPageLayout';
import useRedirectWhenLoggedIn from '@/hooks/auth/useRedirectIfLoggedIn';
import { NextPage } from 'next';
import { CldImage } from 'next-cloudinary';
import Head from 'next/head';
import { BiUser } from 'react-icons/bi';
import { FaUserCircle } from 'react-icons/fa';
const RegisterUserPage: NextPage = () => {
useRedirectWhenLoggedIn();
@@ -15,14 +16,26 @@ const RegisterUserPage: NextPage = () => {
<title>Register User</title>
<meta name="description" content="Register a new user" />
</Head>
<FormPageLayout
headingText="Register User"
headingIcon={BiUser}
backLink="/"
backLinkText="Back to home"
>
<div className="relative flex min-h-dvh w-full flex-col items-center justify-center bg-base-300">
<CldImage
src="https://res.cloudinary.com/dxie9b7na/image/upload/v1701056793/cloudinary-images/pexels-elevate-1267700_jrno3s.jpg"
alt="Login Image"
width={5000}
height={5000}
className="pointer-events-none absolute h-full w-full object-cover mix-blend-overlay"
/>
<div className="relative flex w-10/12 flex-col items-center pb-20 text-center text-base-content lg:w-6/12">
<div className="mb-8 mt-20 flex w-full flex-col items-center space-y-2 text-center">
<FaUserCircle className="text-6xl text-primary-content" />
<h1 className="text-6xl font-extrabold">Register</h1>
</div>
<div className="w-full">
<RegisterUserForm />
</FormPageLayout>
</div>
</div>
</div>
</>
);
};

View File

@@ -25,7 +25,7 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ user }) => {
<meta name="description" content="User information" />
</Head>
<>
<main className="mb-12 mt-10 flex w-full flex-col items-center justify-center">
<main className="mb-12 mt-16 flex w-full flex-col items-center justify-center">
<div className="w-11/12 space-y-3 xl:w-9/12 2xl:w-8/12">
<UserHeader user={user} />
</div>

View File

@@ -22,8 +22,8 @@ import useGetUsersFollowedByUser from '@/hooks/data-fetching/user-follows/useGet
import useGetUsersFollowingUser from '@/hooks/data-fetching/user-follows/useGetUsersFollowingUser';
import UpdateProfileSchema from '@/services/users/auth/schema/UpdateProfileSchema';
import sendUpdateUserAvatarRequest from '@/requests/Account/sendUpdateUserAvatarRequest';
import sendUpdateUserProfileRequest from '@/requests/Account/sendUpdateUserProfileRequest';
import sendUpdateUserAvatarRequest from '@/requests/users/profile/sendUpdateUserAvatarRequest';
import sendUpdateUserProfileRequest from '@/requests/users/profile/sendUpdateUserProfileRequest';
import Spinner from '@/components/ui/Spinner';
const ProfilePage: NextPage = () => {

View File

@@ -33,7 +33,7 @@ const AccountPage: NextPage = () => {
content="Your account page. Here you can view your account information, change your settings, and view your posts."
/>
</Head>
<div className="flex flex-col items-center">
<div className="mt-10 flex min-h-dvh flex-col items-center">
<div className="m-12 flex w-11/12 flex-col items-center justify-center space-y-3 lg:w-8/12">
<div className="flex flex-col items-center space-y-3">
<div className="mb-1 h-28 w-28">
@@ -48,22 +48,18 @@ const AccountPage: NextPage = () => {
<div className="h-full w-full">
<Tab.Group>
<Tab.List className="tabs-boxed tabs items-center justify-center rounded-2xl">
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Account
</Tab>
<Tab className="tab tab-md w-1/2 uppercase ui-selected:tab-active">
Your Posts
</Tab>
<Tab.List className="tabs-boxed tabs grid grid-cols-2">
<Tab className="tab uppercase ui-selected:tab-active">Account</Tab>
<Tab className="tab uppercase ui-selected:tab-active">Your Posts</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel className="h-full space-y-5">
<Tab.Panel className="mt-3 h-full space-y-3">
<UpdateProfileLink />
<AccountInfo pageState={pageState} dispatch={dispatch} />
<Security pageState={pageState} dispatch={dispatch} />
<DeleteAccount pageState={pageState} dispatch={dispatch} />
</Tab.Panel>
<Tab.Panel className="h-full space-y-5">
<Tab.Panel className="h-full">
<UserPosts />
</Tab.Panel>
</Tab.Panels>

View File

@@ -22,7 +22,7 @@ const ProtectedPage: NextPage = () => {
<Head>
<title>Hello! | The Biergarten App</title>
</Head>
<div className="flex h-full flex-col items-center justify-center space-y-3 bg-primary text-center">
<div className="h-dvh flex flex-col items-center justify-center space-y-3 bg-base-100 text-center">
{isLoading && <Spinner size={isDesktop ? 'xl' : 'md'} />}
{user && !isLoading && (
<>

View File

@@ -12,8 +12,9 @@ import { NextPage } from 'next';
import { SubmitHandler, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useRouter } from 'next/router';
import sendForgotPasswordRequest from '@/requests/User/sendForgotPasswordRequest';
import { FaUserCircle } from 'react-icons/fa';
import { sendForgotPasswordRequest } from '@/requests/users/auth';
interface ForgotPasswordPageProps {}
@@ -29,7 +30,7 @@ const ForgotPasswordPage: NextPage<ForgotPasswordPageProps> = () => {
const onSubmit: SubmitHandler<{ email: string }> = async (data) => {
try {
const loadingToast = toast.loading('Sending reset link...');
await sendForgotPasswordRequest(data.email);
await sendForgotPasswordRequest({ email: data.email });
reset();
toast.dismiss(loadingToast);
toast.success('Password reset link sent!');

View File

@@ -1,53 +0,0 @@
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const BeerCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
beerPostId: z.string().cuid(),
});
/**
* Sends a POST request to the server to create a new beer comment.
*
* @param data The data to be sent to the server.
* @param data.beerPostId The ID of the beer post to comment on.
* @param data.content The content of the comment.
* @param data.rating The rating of the beer.
* @returns A promise that resolves to the created comment.
* @throws An error if the request fails, the API response is invalid, or the API response
* payload is invalid.
*/
const sendCreateBeerCommentRequest = async ({
beerPostId,
content,
rating,
}: z.infer<typeof BeerCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/beers/${beerPostId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beerPostId, content, rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) {
throw new Error('Invalid API response');
}
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
export default sendCreateBeerCommentRequest;

View File

@@ -1,29 +0,0 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
/**
* Sends a DELETE request to the server to delete a beer post with the given ID.
*
* @param id The ID of the beer post to delete.
* @returns A Promise that resolves to the parsed API response.
* @throws An error if the response fails or the API response is invalid.
*/
const deleteBeerPostRequest = async (id: string) => {
const response = await fetch(`/api/beers/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('Could not successfully parse the response.');
}
return parsed;
};
export default deleteBeerPostRequest;

View File

@@ -1,67 +0,0 @@
import BeerPostQueryResult from '@/services/posts/beer-post/schema/BeerPostQueryResult';
import CreateBeerPostValidationSchema from '@/services/posts/beer-post/schema/CreateBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
/**
* Sends a POST request to the server to create a new beer post.
*
* @param data Data containing the beer post information to be sent to the server.
* @param abv The alcohol by volume of the beer.
* @param breweryId The ID of the brewery that produces the beer.
* @param description The description of the beer.
* @param ibu The International Bitterness Units of the beer.
* @param name The name of the beer.
* @param styleId The ID of the beer style.
* @returns A Promise that resolves to the created beer post.
* @throws An error if the request fails, the API response is invalid, or the API response
* payload is invalid.
*/
const sendCreateBeerPostRequest = async ({
abv,
breweryId,
description,
ibu,
name,
styleId,
}: z.infer<typeof CreateBeerPostValidationSchema>) => {
const response = await fetch('/api/beers/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
abv,
breweryId,
description,
ibu,
name,
styleId,
}),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('Invalid API response');
}
const { payload, success, message } = parsed.data;
if (!success) {
throw new Error(message);
}
const parsedPayload = BeerPostQueryResult.safeParse(payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
export default sendCreateBeerPostRequest;

View File

@@ -1,42 +0,0 @@
import EditBeerPostValidationSchema from '@/services/posts/beer-post/schema/EditBeerPostValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
/**
* Sends a PUT request to the server to update a beer post.
*
* @param data Data containing the updated beer post information to be sent to the server.
* @param data.abv The updated ABV of the beer.
* @param data.description The updated description of the beer.
* @param data.ibu The updated IBU of the beer.
* @param data.id The ID of the beer post to be updated.
* @param data.name The updated name of the beer.
* @param data.styleId The updated style ID of the beer.
* @throws If the response status is not ok or the API response is not successful.
*/
const sendEditBeerPostRequest = async ({
abv,
description,
ibu,
id,
name,
}: z.infer<typeof EditBeerPostValidationSchema>) => {
const response = await fetch(`/api/beers/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ abv, description, ibu, name, id }),
});
if (!response.ok) {
throw new Error(`${response.status}: ${response.statusText}`);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error(parsed.error.message);
}
};
export default sendEditBeerPostRequest;

View File

@@ -1,53 +0,0 @@
import CommentQueryResult from '@/services/schema/CommentSchema/CommentQueryResult';
import CreateCommentValidationSchema from '@/services/schema/CommentSchema/CreateCommentValidationSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const BeerStyleCommentValidationSchemaWithId = CreateCommentValidationSchema.extend({
beerStyleId: z.string().cuid(),
});
/**
* Sends a POST request to the server to create a new beer comment.
*
* @param data The data to be sent to the server.
* @param data.beerPostId The ID of the beer post to comment on.
* @param data.content The content of the comment.
* @param data.rating The rating of the beer.
* @returns A promise that resolves to the created comment.
* @throws An error if the request fails, the API response is invalid, or the API response
* payload is invalid.
*/
const sendCreateBeerStyleCommentRequest = async ({
beerStyleId,
content,
rating,
}: z.infer<typeof BeerStyleCommentValidationSchemaWithId>) => {
const response = await fetch(`/api/beers/styles/${beerStyleId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ beerStyleId, content, rating }),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = await response.json();
const parsedResponse = APIResponseValidationSchema.safeParse(data);
if (!parsedResponse.success) {
throw new Error('Invalid API response');
}
const parsedPayload = CommentQueryResult.safeParse(parsedResponse.data.payload);
if (!parsedPayload.success) {
throw new Error('Invalid API response payload');
}
return parsedPayload.data;
};
export default sendCreateBeerStyleCommentRequest;

View File

@@ -1,34 +0,0 @@
import BreweryPostQueryResult from '@/services/posts/brewery-post/schema/BreweryPostQueryResult';
import CreateBreweryPostSchema from '@/services/posts/brewery-post/schema/CreateBreweryPostSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const sendCreateBreweryPostRequest = async (
data: z.infer<typeof CreateBreweryPostSchema>,
) => {
const response = await fetch('/api/breweries/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response parsing failed');
}
const parsedPayload = BreweryPostQueryResult.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
throw new Error('API response payload parsing failed');
}
return parsedPayload.data;
};
export default sendCreateBreweryPostRequest;

View File

@@ -1,35 +0,0 @@
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
interface SendEditUserRequestArgs {
user: z.infer<typeof GetUserSchema>;
data: {
username: string;
email: string;
firstName: string;
lastName: string;
};
}
const sendEditUserRequest = async ({ user, data }: SendEditUserRequestArgs) => {
const response = await fetch(`/api/users/${user!.id}`, {
body: JSON.stringify(data),
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response validation failed.');
}
return parsed;
};
export default sendEditUserRequest;

View File

@@ -1,24 +0,0 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
const sendForgotPasswordRequest = async (email: string) => {
const response = await fetch('/api/users/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!response.ok) {
throw new Error("Something went wrong and we couldn't send the reset link.");
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error(parsed.error.message);
}
return parsed.data;
};
export default sendForgotPasswordRequest;

View File

@@ -1,31 +0,0 @@
import { BasicUserInfoSchema } from '@/config/auth/types';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
const sendLoginUserRequest = async (data: { username: string; password: string }) => {
const response = await fetch('/api/users/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
const json: unknown = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response validation failed');
}
if (!parsed.data.success) {
throw new Error(parsed.data.message);
}
const parsedPayload = BasicUserInfoSchema.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
throw new Error('API response payload validation failed');
}
return parsedPayload.data;
};
export default sendLoginUserRequest;

View File

@@ -1,33 +0,0 @@
import { CreateUserValidationSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import GetUserSchema from '@/services/users/auth/schema/GetUserSchema';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
async function sendRegisterUserRequest(data: z.infer<typeof CreateUserValidationSchema>) {
const response = await fetch('/api/users/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response validation failed.');
}
if (!parsed.data.success) {
throw new Error(parsed.data.message);
}
const parsedPayload = GetUserSchema.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
throw new Error('API response payload validation failed.');
}
return parsedPayload.data;
}
export default sendRegisterUserRequest;

View File

@@ -1,26 +0,0 @@
import { UpdatePasswordSchema } from '@/services/users/auth/schema/CreateUserValidationSchemas';
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const sendUpdatePasswordRequest = async (data: z.infer<typeof UpdatePasswordSchema>) => {
const response = await fetch('/api/users/edit-password', {
body: JSON.stringify(data),
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) {
throw new Error(response.statusText);
}
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
throw new Error('API response validation failed.');
}
return parsed.data;
};
export default sendUpdatePasswordRequest;

View File

@@ -1,25 +0,0 @@
import APIResponseValidationSchema from '@/validation/APIResponseValidationSchema';
import { z } from 'zod';
const validateEmailRequest = async (email: string) => {
const response = await fetch(`/api/users/check-email?email=${email}`);
const json = await response.json();
const parsed = APIResponseValidationSchema.safeParse(json);
if (!parsed.success) {
return false;
}
const parsedPayload = z
.object({ emailIsTaken: z.boolean() })
.safeParse(parsed.data.payload);
if (!parsedPayload.success) {
return false;
}
return !parsedPayload.data.emailIsTaken;
};
export default validateEmailRequest;

Some files were not shown because too many files have changed in this diff Show More