From 2a54be8e01564a7166135684c81aaf6f92f1537e Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Wed, 25 Sep 2024 17:58:56 +0200 Subject: [PATCH 01/16] chore: fix vulnerabilities with npm audit fix --- package-lock.json | 208 ++++++++++++++++++++++++++-------------------- 1 file changed, 118 insertions(+), 90 deletions(-) diff --git a/package-lock.json b/package-lock.json index a455380..44a8c0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2639,208 +2639,224 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", - "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", - "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", - "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", - "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", - "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", - "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", - "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", - "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", - "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", - "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", - "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", - "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", - "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", - "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", - "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", - "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -6531,10 +6547,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -6759,6 +6776,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -7006,10 +7024,11 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -7035,9 +7054,10 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -7052,9 +7072,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -7070,10 +7090,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -7446,10 +7467,11 @@ } }, "node_modules/rollup": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz", - "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -7461,22 +7483,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.19.1", - "@rollup/rollup-android-arm64": "4.19.1", - "@rollup/rollup-darwin-arm64": "4.19.1", - "@rollup/rollup-darwin-x64": "4.19.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", - "@rollup/rollup-linux-arm-musleabihf": "4.19.1", - "@rollup/rollup-linux-arm64-gnu": "4.19.1", - "@rollup/rollup-linux-arm64-musl": "4.19.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", - "@rollup/rollup-linux-riscv64-gnu": "4.19.1", - "@rollup/rollup-linux-s390x-gnu": "4.19.1", - "@rollup/rollup-linux-x64-gnu": "4.19.1", - "@rollup/rollup-linux-x64-musl": "4.19.1", - "@rollup/rollup-win32-arm64-msvc": "4.19.1", - "@rollup/rollup-win32-ia32-msvc": "4.19.1", - "@rollup/rollup-win32-x64-msvc": "4.19.1", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -7604,10 +7626,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -8106,14 +8129,15 @@ } }, "node_modules/vite": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", - "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -8132,6 +8156,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -8149,6 +8174,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, From 5098b4ea74c86f534d8aedef734d996752d12836 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Wed, 25 Sep 2024 20:08:12 +0200 Subject: [PATCH 02/16] refactor: Add infinite query --- src/api.ts | 133 ++++++++------- src/components/meme-card.tsx | 9 ++ src/hooks/use-feed.ts | 33 ++++ src/routes/_authentication/index.tsx | 234 ++------------------------- src/routes/login.tsx | 10 +- 5 files changed, 137 insertions(+), 282 deletions(-) create mode 100644 src/components/meme-card.tsx create mode 100644 src/hooks/use-feed.ts diff --git a/src/api.ts b/src/api.ts index 9bb78c9..1dc1a89 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,13 +2,13 @@ const BASE_URL = import.meta.env.VITE_API_BASE_URL as string; export class UnauthorizedError extends Error { constructor() { - super('Unauthorized'); + super("Unauthorized"); } } export class NotFoundError extends Error { constructor() { - super('Not Found'); + super("Not Found"); } } @@ -23,77 +23,90 @@ function checkStatus(response: Response) { } export type LoginResponse = { - jwt: string -} + jwt: string; +}; /** * Authenticate the user with the given credentials - * @param username - * @param password - * @returns + * @param username + * @param password + * @returns */ -export async function login(username: string, password: string): Promise { +export async function login( + username: string, + password: string +): Promise { return await fetch(`${BASE_URL}/authentication/login`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json' + "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), - }).then(res => checkStatus(res).json()) + }).then((res) => checkStatus(res).json()); } export type GetUserByIdResponse = { id: string; username: string; pictureUrl: string; -} +}; /** * Get a user by their id - * @param token - * @param id - * @returns + * @param token + * @param id + * @returns */ -export async function getUserById(token: string, id: string): Promise { +export async function getUserById( + token: string, + id: string +): Promise { return await fetch(`${BASE_URL}/users/${id}`, { headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }).then(res => checkStatus(res).json()) + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }).then((res) => checkStatus(res).json()); } +export type MemeText = { + content: string; + x: number; + y: number; +}; + +export type Meme = { + id: string; + authorId: string; + pictureUrl: string; + description: string; + commentsCount: string; + texts: MemeText[]; + createdAt: string; +}; + export type GetMemesResponse = { total: number; pageSize: number; - results: { - id: string; - authorId: string; - pictureUrl: string; - description: string; - commentsCount: string; - texts: { - content: string; - x: number; - y: number; - }[]; - createdAt: string; - }[] -} + results: Meme[]; +}; /** * Get the list of memes for a given page - * @param token - * @param page - * @returns + * @param token + * @param page + * @returns */ -export async function getMemes(token: string, page: number): Promise { +export async function getMemes( + token: string, + page: number +): Promise { return await fetch(`${BASE_URL}/memes?page=${page}`, { headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }).then(res => checkStatus(res).json()) + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }).then((res) => checkStatus(res).json()); } export type GetMemeCommentsResponse = { @@ -105,8 +118,8 @@ export type GetMemeCommentsResponse = { memeId: string; content: string; createdAt: string; - }[] -} + }[]; +}; /** * Get comments for a meme @@ -114,13 +127,17 @@ export type GetMemeCommentsResponse = { * @param memeId * @returns */ -export async function getMemeComments(token: string, memeId: string, page: number): Promise { +export async function getMemeComments( + token: string, + memeId: string, + page: number +): Promise { return await fetch(`${BASE_URL}/memes/${memeId}/comments?page=${page}`, { headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }).then(res => checkStatus(res).json()) + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }).then((res) => checkStatus(res).json()); } export type CreateCommentResponse = { @@ -129,7 +146,7 @@ export type CreateCommentResponse = { createdAt: string; authorId: string; memeId: string; -} +}; /** * Create a comment for a meme @@ -137,13 +154,17 @@ export type CreateCommentResponse = { * @param memeId * @param content */ -export async function createMemeComment(token: string, memeId: string, content: string): Promise { +export async function createMemeComment( + token: string, + memeId: string, + content: string +): Promise { return await fetch(`${BASE_URL}/memes/${memeId}/comments`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, }, body: JSON.stringify({ content }), - }).then(res => checkStatus(res).json()); -} \ No newline at end of file + }).then((res) => checkStatus(res).json()); +} diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx new file mode 100644 index 0000000..16f91fe --- /dev/null +++ b/src/components/meme-card.tsx @@ -0,0 +1,9 @@ +import { Text } from "@chakra-ui/react"; +import { Meme } from "../api"; + +type MemeCardProps = { + meme: Meme; +}; +export const MemeCard: React.FC = ({ meme }) => { + return {meme.description}; +}; diff --git a/src/hooks/use-feed.ts b/src/hooks/use-feed.ts new file mode 100644 index 0000000..63d8d27 --- /dev/null +++ b/src/hooks/use-feed.ts @@ -0,0 +1,33 @@ +import { useCallback, useState } from "react"; +import { useAuthToken } from "../contexts/authentication"; +import { + keepPreviousData, + useInfiniteQuery, + useQuery, +} from "@tanstack/react-query"; +import { getMemes, GetMemesResponse } from "../api"; + +function getNextPageParam( + lastPage: GetMemesResponse, + pages: GetMemesResponse[] +) { + const totalPages = Math.ceil(lastPage.total / lastPage.pageSize); + return pages.length + 1 <= totalPages ? pages.length + 1 : undefined; +} + +export function useFeed() { + const token = useAuthToken(); + + const { fetchNextPage, data, isFetching, hasNextPage } = useInfiniteQuery({ + queryKey: ["memes"], + initialPageParam: 1, + queryFn: ({ pageParam }) => getMemes(token, pageParam), + getNextPageParam, + }); + + return { + hasNextPage, + feed: data, + fetchNextPage, + }; +} diff --git a/src/routes/_authentication/index.tsx b/src/routes/_authentication/index.tsx index 6ac8415..30e7fbb 100644 --- a/src/routes/_authentication/index.tsx +++ b/src/routes/_authentication/index.tsx @@ -1,97 +1,12 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { Flex, StackDivider, VStack, Text } from "@chakra-ui/react"; import { createFileRoute } from "@tanstack/react-router"; -import { - Avatar, - Box, - Collapse, - Flex, - Icon, - LinkBox, - LinkOverlay, - StackDivider, - Text, - Input, - VStack, -} from "@chakra-ui/react"; -import { CaretDown, CaretUp, Chat } from "@phosphor-icons/react"; -import { format } from "timeago.js"; -import { - createMemeComment, - getMemeComments, - GetMemeCommentsResponse, - getMemes, - GetMemesResponse, - getUserById, - GetUserByIdResponse, -} from "../../api"; -import { useAuthToken } from "../../contexts/authentication"; -import { Loader } from "../../components/loader"; -import { MemePicture } from "../../components/meme-picture"; -import { useState } from "react"; -import { jwtDecode } from "jwt-decode"; +import { useFeed } from "../../hooks/use-feed"; +import React from "react"; +import { MemeCard } from "../../components/meme-card"; export const MemeFeedPage: React.FC = () => { - const token = useAuthToken(); - const { isLoading, data: memes } = useQuery({ - queryKey: ["memes"], - queryFn: async () => { - const memes: GetMemesResponse["results"] = []; - const firstPage = await getMemes(token, 1); - memes.push(...firstPage.results); - const remainingPages = - Math.ceil(firstPage.total / firstPage.pageSize) - 1; - for (let i = 0; i < remainingPages; i++) { - const page = await getMemes(token, i + 2); - memes.push(...page.results); - } - const memesWithAuthorAndComments = []; - for (let meme of memes) { - const author = await getUserById(token, meme.authorId); - const comments: GetMemeCommentsResponse["results"] = []; - const firstPage = await getMemeComments(token, meme.id, 1); - comments.push(...firstPage.results); - const remainingCommentPages = - Math.ceil(firstPage.total / firstPage.pageSize) - 1; - for (let i = 0; i < remainingCommentPages; i++) { - const page = await getMemeComments(token, meme.id, i + 2); - comments.push(...page.results); - } - const commentsWithAuthor: (GetMemeCommentsResponse["results"][0] & { - author: GetUserByIdResponse; - })[] = []; - for (let comment of comments) { - const author = await getUserById(token, comment.authorId); - commentsWithAuthor.push({ ...comment, author }); - } - memesWithAuthorAndComments.push({ - ...meme, - author, - comments: commentsWithAuthor, - }); - } - return memesWithAuthorAndComments; - }, - }); - const { data: user } = useQuery({ - queryKey: ["user"], - queryFn: async () => { - return await getUserById(token, jwtDecode<{ id: string }>(token).id); - }, - }); - const [openedCommentSection, setOpenedCommentSection] = useState< - string | null - >(null); - const [commentContent, setCommentContent] = useState<{ - [key: string]: string; - }>({}); - const { mutate } = useMutation({ - mutationFn: async (data: { memeId: string; content: string }) => { - await createMemeComment(token, data.memeId, data.content); - }, - }); - if (isLoading) { - return ; - } + const { feed, fetchNextPage, hasNextPage } = useFeed(); + console.log("hasNextPage", hasNextPage); return ( { maxWidth={800} divider={} > - {memes?.map((meme) => { - return ( - - - - - {meme.author.username} - - - {format(meme.createdAt)} - - - - - - Description:{" "} - - - - {meme.description} - - - - - - - - setOpenedCommentSection( - openedCommentSection === meme.id ? null : meme.id, - ) - } - > - {meme.commentsCount} comments - - - - - - - - -
{ - event.preventDefault(); - if (commentContent[meme.id]) { - mutate({ - memeId: meme.id, - content: commentContent[meme.id], - }); - } - }} - > - - - { - setCommentContent({ - ...commentContent, - [meme.id]: event.target.value, - }); - }} - value={commentContent[meme.id]} - /> - -
-
- - {meme.comments.map((comment) => ( - - - - - - {comment.author.username} - - - {format(comment.createdAt)} - - - - {comment.content} - - - - ))} - -
-
- ); + {feed?.pages.map((page) => { + return page.results.map((meme) => { + return ; + }); })} + + feed2
); diff --git a/src/routes/login.tsx b/src/routes/login.tsx index e7712bb..ce21459 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -41,7 +41,7 @@ export const LoginPage: React.FC = () => { mutationFn: (data: Inputs) => login(data.username, data.password), onSuccess: ({ jwt }) => { authenticate(jwt); - } + }, }); const { register, handleSubmit } = useForm(); const onSubmit: SubmitHandler = async (data) => { @@ -49,7 +49,8 @@ export const LoginPage: React.FC = () => { }; if (state.isAuthenticated) { - return ; + console.log("auth redirect", redirect); + return ; } return ( @@ -115,8 +116,9 @@ export const LoginPage: React.FC = () => { export const Route = createFileRoute("/login")({ validateSearch: (search): SearchParams => { return { - redirect: typeof search.redirect === "string" ? search.redirect : undefined, - } + redirect: + typeof search.redirect === "string" ? search.redirect : undefined, + }; }, component: LoginPage, }); From 3da068984303c1d259f044c729ddb5b7fa3af0f9 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Wed, 25 Sep 2024 22:59:16 +0200 Subject: [PATCH 03/16] refactor: Add MemeCard component --- src/api.ts | 54 ++--------- src/components/meme-author.tsx | 33 +++++++ src/components/meme-card.tsx | 153 ++++++++++++++++++++++++++++++- src/hooks/use-feed.ts | 17 ++-- src/hooks/use-user.ts | 11 +++ src/types/meme-comment.ts | 7 ++ src/types/meme-text.ts | 5 + src/types/meme.ts | 11 +++ src/types/pagination-response.ts | 5 + src/types/user.ts | 5 + 10 files changed, 240 insertions(+), 61 deletions(-) create mode 100644 src/components/meme-author.tsx create mode 100644 src/hooks/use-user.ts create mode 100644 src/types/meme-comment.ts create mode 100644 src/types/meme-text.ts create mode 100644 src/types/meme.ts create mode 100644 src/types/pagination-response.ts create mode 100644 src/types/user.ts diff --git a/src/api.ts b/src/api.ts index 1dc1a89..e88bb40 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,3 +1,8 @@ +import { Meme } from "./types/meme"; +import { MemeComment } from "./types/meme-comment"; +import { PaginationResponse } from "./types/pagination-response"; +import { User } from "./types/user"; + const BASE_URL = import.meta.env.VITE_API_BASE_URL as string; export class UnauthorizedError extends Error { @@ -45,22 +50,13 @@ export async function login( }).then((res) => checkStatus(res).json()); } -export type GetUserByIdResponse = { - id: string; - username: string; - pictureUrl: string; -}; - /** * Get a user by their id * @param token * @param id * @returns */ -export async function getUserById( - token: string, - id: string -): Promise { +export async function getUserById(token: string, id: string): Promise { return await fetch(`${BASE_URL}/users/${id}`, { headers: { "Content-Type": "application/json", @@ -69,28 +65,6 @@ export async function getUserById( }).then((res) => checkStatus(res).json()); } -export type MemeText = { - content: string; - x: number; - y: number; -}; - -export type Meme = { - id: string; - authorId: string; - pictureUrl: string; - description: string; - commentsCount: string; - texts: MemeText[]; - createdAt: string; -}; - -export type GetMemesResponse = { - total: number; - pageSize: number; - results: Meme[]; -}; - /** * Get the list of memes for a given page * @param token @@ -100,7 +74,7 @@ export type GetMemesResponse = { export async function getMemes( token: string, page: number -): Promise { +): Promise> { return await fetch(`${BASE_URL}/memes?page=${page}`, { headers: { "Content-Type": "application/json", @@ -109,18 +83,6 @@ export async function getMemes( }).then((res) => checkStatus(res).json()); } -export type GetMemeCommentsResponse = { - total: number; - pageSize: number; - results: { - id: string; - authorId: string; - memeId: string; - content: string; - createdAt: string; - }[]; -}; - /** * Get comments for a meme * @param token @@ -131,7 +93,7 @@ export async function getMemeComments( token: string, memeId: string, page: number -): Promise { +): Promise> { return await fetch(`${BASE_URL}/memes/${memeId}/comments?page=${page}`, { headers: { "Content-Type": "application/json", diff --git a/src/components/meme-author.tsx b/src/components/meme-author.tsx new file mode 100644 index 0000000..5b4e9ba --- /dev/null +++ b/src/components/meme-author.tsx @@ -0,0 +1,33 @@ +import { Avatar, Flex, Text } from "@chakra-ui/react"; +import { useUser } from "../hooks/use-user"; + +type MemeAuthorProps = { + authorId: string; + nameTestId?: string; +}; + +export const MemeAuthor: React.FC = ({ + authorId, + nameTestId, +}) => { + const { data: author } = useUser(authorId); + + if (!author) { + return null; + } + + return ( + + + + {author.username} + + + ); +}; diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx index 16f91fe..b65b801 100644 --- a/src/components/meme-card.tsx +++ b/src/components/meme-card.tsx @@ -1,9 +1,152 @@ -import { Text } from "@chakra-ui/react"; -import { Meme } from "../api"; +import { + Avatar, + Box, + Collapse, + Flex, + Icon, + LinkBox, + LinkOverlay, + Text, + VStack, +} from "@chakra-ui/react"; -type MemeCardProps = { +import { MemePicture } from "./meme-picture"; +import { format } from "timeago.js"; +import { Meme } from "../types/meme"; +import { MemeAuthor } from "./meme-author"; + +type MemeCardrops = { meme: Meme; }; -export const MemeCard: React.FC = ({ meme }) => { - return {meme.description}; +export const MemeCard: React.FC = ({ meme }) => { + return ( + + + + + {format(meme.createdAt)} + + + + + + Description:{" "} + + + + {meme.description} + + + + + {/* + + + + setOpenedCommentSection( + openedCommentSection === meme.id ? null : meme.id + ) + } + > + + {meme.commentsCount} comments + + + + + + + */} + + {/* + +
{ + event.preventDefault(); + if (commentContent[meme.id]) { + mutate({ + memeId: meme.id, + content: commentContent[meme.id], + }); + } + }} + > + + + { + setCommentContent({ + ...commentContent, + [meme.id]: event.target.value, + }); + }} + value={commentContent[meme.id]} + /> + +
+
+ + {meme.comments.map((comment) => ( + + + + + + + {comment.author.username} + + + + {format(comment.createdAt)} + + + + {comment.content} + + + + ))} + +
*/} +
+ ); }; diff --git a/src/hooks/use-feed.ts b/src/hooks/use-feed.ts index 63d8d27..19ac6c5 100644 --- a/src/hooks/use-feed.ts +++ b/src/hooks/use-feed.ts @@ -1,15 +1,12 @@ -import { useCallback, useState } from "react"; import { useAuthToken } from "../contexts/authentication"; -import { - keepPreviousData, - useInfiniteQuery, - useQuery, -} from "@tanstack/react-query"; -import { getMemes, GetMemesResponse } from "../api"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { getMemes } from "../api"; +import { PaginationResponse } from "../types/pagination-response"; +import { Meme } from "../types/meme"; function getNextPageParam( - lastPage: GetMemesResponse, - pages: GetMemesResponse[] + lastPage: PaginationResponse, + pages: PaginationResponse[] ) { const totalPages = Math.ceil(lastPage.total / lastPage.pageSize); return pages.length + 1 <= totalPages ? pages.length + 1 : undefined; @@ -18,7 +15,7 @@ function getNextPageParam( export function useFeed() { const token = useAuthToken(); - const { fetchNextPage, data, isFetching, hasNextPage } = useInfiniteQuery({ + const { fetchNextPage, data, hasNextPage } = useInfiniteQuery({ queryKey: ["memes"], initialPageParam: 1, queryFn: ({ pageParam }) => getMemes(token, pageParam), diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts new file mode 100644 index 0000000..0150a73 --- /dev/null +++ b/src/hooks/use-user.ts @@ -0,0 +1,11 @@ +import { useQuery } from "@tanstack/react-query"; +import { getUserById } from "../api"; +import { useAuthToken } from "../contexts/authentication"; + +export function useUser(userId: string) { + const token = useAuthToken(); + return useQuery({ + queryKey: ["user", userId], + queryFn: () => getUserById(token, userId), + }); +} diff --git a/src/types/meme-comment.ts b/src/types/meme-comment.ts new file mode 100644 index 0000000..f4a39b9 --- /dev/null +++ b/src/types/meme-comment.ts @@ -0,0 +1,7 @@ +export type MemeComment = { + id: string; + authorId: string; + memeId: string; + content: string; + createdAt: string; +}; diff --git a/src/types/meme-text.ts b/src/types/meme-text.ts new file mode 100644 index 0000000..fc97f89 --- /dev/null +++ b/src/types/meme-text.ts @@ -0,0 +1,5 @@ +export type MemeText = { + content: string; + x: number; + y: number; +}; diff --git a/src/types/meme.ts b/src/types/meme.ts new file mode 100644 index 0000000..e214de3 --- /dev/null +++ b/src/types/meme.ts @@ -0,0 +1,11 @@ +import { MemeText } from "./meme-text"; + +export type Meme = { + id: string; + authorId: string; + pictureUrl: string; + description: string; + commentsCount: string; + texts: MemeText[]; + createdAt: string; +}; diff --git a/src/types/pagination-response.ts b/src/types/pagination-response.ts new file mode 100644 index 0000000..d8214e0 --- /dev/null +++ b/src/types/pagination-response.ts @@ -0,0 +1,5 @@ +export type PaginationResponse = { + total: number; + pageSize: number; + results: T[]; +}; diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..49c9eed --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,5 @@ +export type User = { + id: string; + username: string; + pictureUrl: string; +}; From 1178b14c9852ca55b641902a0fdd00ba8969a5d5 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Fri, 27 Sep 2024 14:16:03 +0200 Subject: [PATCH 04/16] refactor: Improve pagination --- package-lock.json | 16 ++ package.json | 1 + src/components/comment-form.tsx | 51 +++++ src/components/comment-list.tsx | 45 +++++ src/components/meme-card-comments.tsx | 56 ++++++ src/components/meme-card.tsx | 28 +-- src/components/meme-comment-item.tsx | 49 +++++ src/components/meme-list.tsx | 53 ++++++ src/components/meme-picture.tsx | 2 +- src/contexts/scroll-ref.tsx | 35 ++++ src/hooks/use-comment-list.ts | 14 ++ src/hooks/use-feed.ts | 19 +- src/layouts/meme-list-layout.tsx | 24 +++ src/oldindex.ts | 263 ++++++++++++++++++++++++++ src/routes/_authentication/index.tsx | 27 +-- src/types/meme.ts | 2 +- src/utils/get-next-page-param.ts | 9 + 17 files changed, 642 insertions(+), 52 deletions(-) create mode 100644 src/components/comment-form.tsx create mode 100644 src/components/comment-list.tsx create mode 100644 src/components/meme-card-comments.tsx create mode 100644 src/components/meme-comment-item.tsx create mode 100644 src/components/meme-list.tsx create mode 100644 src/contexts/scroll-ref.tsx create mode 100644 src/hooks/use-comment-list.ts create mode 100644 src/layouts/meme-list-layout.tsx create mode 100644 src/oldindex.ts create mode 100644 src/utils/get-next-page-param.ts diff --git a/package-lock.json b/package-lock.json index 44a8c0e..5c483d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.52.1", + "react-intersection-observer": "^9.13.1", "timeago.js": "^4.0.2" }, "devDependencies": { @@ -7299,6 +7300,21 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-intersection-observer": { + "version": "9.13.1", + "resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.13.1.tgz", + "integrity": "sha512-tSzDaTy0qwNPLJHg8XZhlyHTgGW6drFKTtvjdL+p6um12rcnp8Z5XstE+QNBJ7c64n5o0Lj4ilUleA41bmDoMw==", + "license": "MIT", + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 48c50c1..12c331e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.52.1", + "react-intersection-observer": "^9.13.1", "timeago.js": "^4.0.2" }, "devDependencies": { diff --git a/src/components/comment-form.tsx b/src/components/comment-form.tsx new file mode 100644 index 0000000..9a94a92 --- /dev/null +++ b/src/components/comment-form.tsx @@ -0,0 +1,51 @@ +import { Box } from "@chakra-ui/react"; +import { useCallback } from "react"; + +export const CommentForm: React.FC = () => { + const handleSubmit = useCallback((event) => { + event.preventDefault(); + if (commentContent[meme.id]) { + mutate({ + memeId: meme.id, + content: commentContent[meme.id], + }); + } + }); + + return ( + +
{ + event.preventDefault(); + if (commentContent[meme.id]) { + mutate({ + memeId: meme.id, + content: commentContent[meme.id], + }); + } + }} + > + + + { + setCommentContent({ + ...commentContent, + [meme.id]: event.target.value, + }); + }} + value={commentContent[meme.id]} + /> + +
+
+ ); +}; diff --git a/src/components/comment-list.tsx b/src/components/comment-list.tsx new file mode 100644 index 0000000..01b1047 --- /dev/null +++ b/src/components/comment-list.tsx @@ -0,0 +1,45 @@ +import { VStack } from "@chakra-ui/react"; +import { useCommentList } from "../hooks/use-comment-list"; +import { MemeCommentItem } from "./meme-comment-item"; +import { useInView } from "react-intersection-observer"; +import { useEffect } from "react"; +import { useScrollRef } from "../contexts/scroll-ref"; + +type CommentListProps = { + memeId: string; +}; + +export const CommentList: React.FC = ({ memeId }) => { + const { + data: comments, + fetchNextPage, + hasNextPage, + isFetching, + } = useCommentList(memeId); + + const scrollRef = useScrollRef(); + const { ref, inView } = useInView({ + rootMargin: "200px", + root: scrollRef.ref?.current, + }); + + useEffect(() => { + fetchNextPage(); + }, [inView, fetchNextPage]); + return ( + + {comments?.pages.map((page) => { + return page.results.map((comment) => { + return ( + + ); + }); + })} + {hasNextPage && !isFetching &&
} + + ); +}; diff --git a/src/components/meme-card-comments.tsx b/src/components/meme-card-comments.tsx new file mode 100644 index 0000000..825df07 --- /dev/null +++ b/src/components/meme-card-comments.tsx @@ -0,0 +1,56 @@ +import { + Box, + Collapse, + Flex, + Icon, + LinkBox, + LinkOverlay, + Text, +} from "@chakra-ui/react"; +import { CaretDown, CaretUp, Chat } from "@phosphor-icons/react"; +import { useCallback } from "react"; +import { CommentList } from "./comment-list"; + +type MemeCardCommentsProps = { + memeId: string; + commentCount: number; + opened: boolean; + onOpen: (memeId: string) => void; +}; + +export const MemeCardComments: React.FC = ({ + memeId, + commentCount, + opened, + onOpen, +}) => { + const handleClickOpen = useCallback(() => { + onOpen(memeId); + }, [memeId, onOpen]); + return ( + <> + + + + + + {commentCount > 1 + ? `${commentCount} comments` + : `${commentCount} comment`} + + + + + + + + + + + + ); +}; diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx index b65b801..ab16273 100644 --- a/src/components/meme-card.tsx +++ b/src/components/meme-card.tsx @@ -1,24 +1,21 @@ -import { - Avatar, - Box, - Collapse, - Flex, - Icon, - LinkBox, - LinkOverlay, - Text, - VStack, -} from "@chakra-ui/react"; +import { Box, Flex, Text, VStack } from "@chakra-ui/react"; import { MemePicture } from "./meme-picture"; import { format } from "timeago.js"; import { Meme } from "../types/meme"; import { MemeAuthor } from "./meme-author"; +import { MemeCardComments } from "./meme-card-comments"; type MemeCardrops = { meme: Meme; + commentsOpened: boolean; + onOpenComments: (memeId: string) => void; }; -export const MemeCard: React.FC = ({ meme }) => { +export const MemeCard: React.FC = ({ + meme, + commentsOpened, + onOpenComments, +}) => { return ( @@ -50,6 +47,13 @@ export const MemeCard: React.FC = ({ meme }) => { + + {/* diff --git a/src/components/meme-comment-item.tsx b/src/components/meme-comment-item.tsx new file mode 100644 index 0000000..21a4707 --- /dev/null +++ b/src/components/meme-comment-item.tsx @@ -0,0 +1,49 @@ +import { Avatar, Box, Flex, Text } from "@chakra-ui/react"; +import { MemeComment } from "../types/meme-comment"; +import { format } from "timeago.js"; +import { useUser } from "../hooks/use-user"; + +type MemeCommentProps = { + comment: MemeComment; + memeId: string; +}; + +export const MemeCommentItem: React.FC = ({ + comment, + memeId, +}: MemeCommentProps) => { + const { data: author } = useUser(comment.authorId); + + return ( + + + + + + + {author?.username} + + + + {format(comment.createdAt)} + + + + {comment.content} + + + + ); +}; diff --git a/src/components/meme-list.tsx b/src/components/meme-list.tsx new file mode 100644 index 0000000..588ebbd --- /dev/null +++ b/src/components/meme-list.tsx @@ -0,0 +1,53 @@ +import { useCallback, useEffect, useState } from "react"; +import { useFeed } from "../hooks/use-feed"; +import { MemeCard } from "./meme-card"; +import { useScrollRef } from "../contexts/scroll-ref"; +import { useInView } from "react-intersection-observer"; + +export const MemeList: React.FC = () => { + const { data: feed, fetchNextPage, hasNextPage, isFetching } = useFeed(); + + const [openedCommentsMemeId, setOpenedCommentsMemeId] = useState< + string | null + >(null); + + const handleOpenComments = useCallback((memeId: string) => { + setOpenedCommentsMemeId((prev) => { + return prev === memeId ? null : memeId; + }); + }, []); + + const scrollRef = useScrollRef(); + + const { ref, inView } = useInView({ + root: scrollRef.ref?.current, + rootMargin: "400px 0px", + skip: isFetching || !hasNextPage, + }); + + useEffect(() => { + if (inView) { + fetchNextPage(); + } + }, [inView, fetchNextPage]); + + return ( + <> + {feed?.pages.map((page) => { + return page.results.map((meme) => { + return ( + + ); + }); + })} +
+ hello +
+ + ); +}; diff --git a/src/components/meme-picture.tsx b/src/components/meme-picture.tsx index 6e99619..20135ce 100644 --- a/src/components/meme-picture.tsx +++ b/src/components/meme-picture.tsx @@ -18,7 +18,7 @@ const REF_FONT_SIZE = 36; export const MemePicture: React.FC = ({ pictureUrl, texts: rawTexts, - dataTestId = '', + dataTestId = "", }) => { const containerRef = useRef(null); const dimensions = useDimensions(containerRef, true); diff --git a/src/contexts/scroll-ref.tsx b/src/contexts/scroll-ref.tsx new file mode 100644 index 0000000..228d9d5 --- /dev/null +++ b/src/contexts/scroll-ref.tsx @@ -0,0 +1,35 @@ +import { Flex, FlexProps } from "@chakra-ui/react"; +import { createContext, useRef, useMemo, useContext } from "react"; + +type ScrollRefState = { + ref: React.RefObject | null; +}; + +const ScrollRefContext = createContext(undefined); + +// eslint-disable-next-line react-refresh/only-export-components +export function useScrollRef() { + const context = useContext(ScrollRefContext); + if (!context) { + throw new Error( + "useAuthentication must be used within an AuthenticationProvider" + ); + } + return context; +} + +export const ScrollRefProvider: React.FC< + React.PropsWithChildren +> = ({ children, ...props }) => { + const ref = useRef(null); + + const value = useMemo(() => ({ ref }), [ref]); + + return ( + + + {children} + + + ); +}; diff --git a/src/hooks/use-comment-list.ts b/src/hooks/use-comment-list.ts new file mode 100644 index 0000000..e6bef3b --- /dev/null +++ b/src/hooks/use-comment-list.ts @@ -0,0 +1,14 @@ +import { useAuthToken } from "../contexts/authentication"; +import { getMemeComments } from "../api"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { getNextPageParam } from "../utils/get-next-page-param"; + +export function useCommentList(memeId: string) { + const token = useAuthToken(); + return useInfiniteQuery({ + queryKey: ["comments", memeId], + initialPageParam: 1, + queryFn: ({ pageParam }) => getMemeComments(token, memeId, pageParam), + getNextPageParam, + }); +} diff --git a/src/hooks/use-feed.ts b/src/hooks/use-feed.ts index 19ac6c5..eae1d0d 100644 --- a/src/hooks/use-feed.ts +++ b/src/hooks/use-feed.ts @@ -1,30 +1,15 @@ import { useAuthToken } from "../contexts/authentication"; import { useInfiniteQuery } from "@tanstack/react-query"; import { getMemes } from "../api"; -import { PaginationResponse } from "../types/pagination-response"; -import { Meme } from "../types/meme"; - -function getNextPageParam( - lastPage: PaginationResponse, - pages: PaginationResponse[] -) { - const totalPages = Math.ceil(lastPage.total / lastPage.pageSize); - return pages.length + 1 <= totalPages ? pages.length + 1 : undefined; -} +import { getNextPageParam } from "../utils/get-next-page-param"; export function useFeed() { const token = useAuthToken(); - const { fetchNextPage, data, hasNextPage } = useInfiniteQuery({ + return useInfiniteQuery({ queryKey: ["memes"], initialPageParam: 1, queryFn: ({ pageParam }) => getMemes(token, pageParam), getNextPageParam, }); - - return { - hasNextPage, - feed: data, - fetchNextPage, - }; } diff --git a/src/layouts/meme-list-layout.tsx b/src/layouts/meme-list-layout.tsx new file mode 100644 index 0000000..4546098 --- /dev/null +++ b/src/layouts/meme-list-layout.tsx @@ -0,0 +1,24 @@ +import { StackDivider, VStack } from "@chakra-ui/react"; +import { ScrollRefProvider } from "../contexts/scroll-ref"; + +export const MemeListLayout: React.FC = ({ + children, +}) => { + return ( + + } + > + {children} + + + ); +}; diff --git a/src/oldindex.ts b/src/oldindex.ts new file mode 100644 index 0000000..4d11c5b --- /dev/null +++ b/src/oldindex.ts @@ -0,0 +1,263 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { + Avatar, + Box, + Collapse, + Flex, + Icon, + LinkBox, + LinkOverlay, + StackDivider, + Text, + Input, + VStack, +} from "@chakra-ui/react"; +import { CaretDown, CaretUp, Chat } from "@phosphor-icons/react"; +import { format } from "timeago.js"; +import { + createMemeComment, + getMemeComments, + GetMemeCommentsResponse, + getMemes, + GetMemesResponse, + getUserById, + GetUserByIdResponse, +} from "../../api"; +import { useAuthToken } from "../../contexts/authentication"; +import { Loader } from "../../components/loader"; +import { MemePicture } from "../../components/meme-picture"; +import { useState } from "react"; +import { jwtDecode } from "jwt-decode"; + +export const MemeFeedPage: React.FC = () => { + const token = useAuthToken(); + const { isLoading, data: memes } = useQuery({ + queryKey: ["memes"], + queryFn: async () => { + const memes: GetMemesResponse["results"] = []; + const firstPage = await getMemes(token, 1); + memes.push(...firstPage.results); + const remainingPages = + Math.ceil(firstPage.total / firstPage.pageSize) - 1; + // for (let i = 0; i < remainingPages; i++) { + // const page = await getMemes(token, i + 2); + // memes.push(...page.results); + // } + const memesWithAuthorAndComments = []; + for (let meme of memes) { + const author = await getUserById(token, meme.authorId); + const comments: GetMemeCommentsResponse["results"] = []; + const firstPage = await getMemeComments(token, meme.id, 1); + comments.push(...firstPage.results); + const remainingCommentPages = + Math.ceil(firstPage.total / firstPage.pageSize) - 1; + for (let i = 0; i < remainingCommentPages; i++) { + const page = await getMemeComments(token, meme.id, i + 2); + comments.push(...page.results); + } + const commentsWithAuthor: (GetMemeCommentsResponse["results"][0] & { + author: GetUserByIdResponse; + })[] = []; + for (let comment of comments) { + const author = await getUserById(token, comment.authorId); + commentsWithAuthor.push({ ...comment, author }); + } + memesWithAuthorAndComments.push({ + ...meme, + author, + comments: commentsWithAuthor, + }); + } + return memesWithAuthorAndComments; + }, + }); + const { data: user } = useQuery({ + queryKey: ["user"], + queryFn: async () => { + console.log("fetch user"); + return await getUserById(token, jwtDecode<{ id: string }>(token).id); + }, + }); + const [openedCommentSection, setOpenedCommentSection] = useState< + string | null + >(null); + const [commentContent, setCommentContent] = useState<{ + [key: string]: string; + }>({}); + const { mutate } = useMutation({ + mutationFn: async (data: { memeId: string; content: string }) => { + await createMemeComment(token, data.memeId, data.content); + }, + }); + if (isLoading) { + return ; + } + return ( + + } + > + {memes?.map((meme) => { + return ( + + + + + + {meme.author.username} + + + + {format(meme.createdAt)} + + + + + + Description:{" "} + + + + {meme.description} + + + + + + + + setOpenedCommentSection( + openedCommentSection === meme.id ? null : meme.id + ) + } + > + + {meme.commentsCount} comments + + + + + + + + + +
{ + event.preventDefault(); + if (commentContent[meme.id]) { + mutate({ + memeId: meme.id, + content: commentContent[meme.id], + }); + } + }} + > + + + { + setCommentContent({ + ...commentContent, + [meme.id]: event.target.value, + }); + }} + value={commentContent[meme.id]} + /> + +
+
+ + {meme.comments.map((comment) => ( + + + + + + + {comment.author.username} + + + + {format(comment.createdAt)} + + + + {comment.content} + + + + ))} + +
+
+ ); + })} +
+
+ ); +}; + +export const Route = createFileRoute("/_authentication/")({ + component: MemeFeedPage, +}); diff --git a/src/routes/_authentication/index.tsx b/src/routes/_authentication/index.tsx index 30e7fbb..b68d45e 100644 --- a/src/routes/_authentication/index.tsx +++ b/src/routes/_authentication/index.tsx @@ -1,29 +1,14 @@ -import { Flex, StackDivider, VStack, Text } from "@chakra-ui/react"; import { createFileRoute } from "@tanstack/react-router"; -import { useFeed } from "../../hooks/use-feed"; import React from "react"; -import { MemeCard } from "../../components/meme-card"; + +import { MemeListLayout } from "../../layouts/meme-list-layout"; +import { MemeList } from "../../components/meme-list"; export const MemeFeedPage: React.FC = () => { - const { feed, fetchNextPage, hasNextPage } = useFeed(); - console.log("hasNextPage", hasNextPage); return ( - - } - > - {feed?.pages.map((page) => { - return page.results.map((meme) => { - return ; - }); - })} - - feed2 - - + + + ); }; diff --git a/src/types/meme.ts b/src/types/meme.ts index e214de3..a2f1e2f 100644 --- a/src/types/meme.ts +++ b/src/types/meme.ts @@ -5,7 +5,7 @@ export type Meme = { authorId: string; pictureUrl: string; description: string; - commentsCount: string; + commentsCount: number; texts: MemeText[]; createdAt: string; }; diff --git a/src/utils/get-next-page-param.ts b/src/utils/get-next-page-param.ts new file mode 100644 index 0000000..a7baa45 --- /dev/null +++ b/src/utils/get-next-page-param.ts @@ -0,0 +1,9 @@ +import { PaginationResponse } from "../types/pagination-response"; + +export function getNextPageParam( + lastPage: PaginationResponse, + pages: PaginationResponse[] +) { + const totalPages = Math.ceil(lastPage.total / lastPage.pageSize); + return pages.length + 1 <= totalPages ? pages.length + 1 : undefined; +} From e226aa30fcd6938554dfa640046302c80e65a1e1 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Sat, 28 Sep 2024 22:45:04 +0200 Subject: [PATCH 05/16] refactor: Add comment form component --- src/components/comment-form.tsx | 63 +++++++++++++++------------ src/components/meme-card-comments.tsx | 3 ++ src/hooks/use-my-profile.ts | 8 ++++ src/hooks/use-submit-comment.ts | 45 +++++++++++++++++++ 4 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/hooks/use-my-profile.ts create mode 100644 src/hooks/use-submit-comment.ts diff --git a/src/components/comment-form.tsx b/src/components/comment-form.tsx index 9a94a92..9b7f84d 100644 --- a/src/components/comment-form.tsx +++ b/src/components/comment-form.tsx @@ -1,30 +1,41 @@ -import { Box } from "@chakra-ui/react"; +import { Avatar, Box, Flex, Input } from "@chakra-ui/react"; import { useCallback } from "react"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { User } from "../types/user"; +import { createMemeComment } from "../api"; +import { useMutation } from "@tanstack/react-query"; +import { useSubmitComment } from "../hooks/use-submit-comment"; +import { useMyProfile } from "../hooks/use-my-profile"; -export const CommentForm: React.FC = () => { - const handleSubmit = useCallback((event) => { - event.preventDefault(); - if (commentContent[meme.id]) { - mutate({ - memeId: meme.id, - content: commentContent[meme.id], - }); - } - }); +type CommentFormProps = { + memeId: string; +}; + +type Inputs = { + content: string; +}; + +export const CommentForm: React.FC = ({ memeId }) => { + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm(); + + console.log(",,,memeId", memeId); + const { mutate } = useSubmitComment(memeId); + + const user = useMyProfile(); + + const onSubmit: SubmitHandler = async (data) => { + console.log("on submit"); + mutate(data); + }; return ( -
{ - event.preventDefault(); - if (commentContent[meme.id]) { - mutate({ - memeId: meme.id, - content: commentContent[meme.id], - }); - } - }} - > + { /> { - setCommentContent({ - ...commentContent, - [meme.id]: event.target.value, - }); - }} - value={commentContent[meme.id]} + {...register("content", { required: true })} />
diff --git a/src/components/meme-card-comments.tsx b/src/components/meme-card-comments.tsx index 825df07..29296b6 100644 --- a/src/components/meme-card-comments.tsx +++ b/src/components/meme-card-comments.tsx @@ -10,6 +10,7 @@ import { import { CaretDown, CaretUp, Chat } from "@phosphor-icons/react"; import { useCallback } from "react"; import { CommentList } from "./comment-list"; +import { CommentForm } from "./comment-form"; type MemeCardCommentsProps = { memeId: string; @@ -27,6 +28,7 @@ export const MemeCardComments: React.FC = ({ const handleClickOpen = useCallback(() => { onOpen(memeId); }, [memeId, onOpen]); + return ( <> @@ -49,6 +51,7 @@ export const MemeCardComments: React.FC = ({
+ diff --git a/src/hooks/use-my-profile.ts b/src/hooks/use-my-profile.ts new file mode 100644 index 0000000..b58bcbc --- /dev/null +++ b/src/hooks/use-my-profile.ts @@ -0,0 +1,8 @@ +import { useAuthentication } from "../contexts/authentication"; +import { useUser } from "./use-user"; + +export function useMyProfile() { + const { state } = useAuthentication(); + const { data } = useUser(state.isAuthenticated ? state.userId : "anon"); + return data; +} diff --git a/src/hooks/use-submit-comment.ts b/src/hooks/use-submit-comment.ts new file mode 100644 index 0000000..4d02d88 --- /dev/null +++ b/src/hooks/use-submit-comment.ts @@ -0,0 +1,45 @@ +import { + useInfiniteQuery, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { createMemeComment } from "../api"; +import { useAuthToken } from "../contexts/authentication"; +import { Meme } from "../types/meme"; +import { PaginationResponse } from "../types/pagination-response"; + +type InfiniteData = { + pages: PaginationResponse[]; + pageParams: unknown; +}; + +function incrementeCommentCount(data: InfiniteData, memeId: string) { + return { + ...data, + pages: data.pages.map((page) => ({ + ...page, + results: page.results.map((meme) => + meme.id === memeId + ? { ...meme, commentsCount: meme.commentsCount + 1 } + : meme + ), + })), + }; +} + +export function useSubmitComment(memeId: string) { + const queryClient = useQueryClient(); + const token = useAuthToken(); + return useMutation({ + mutationFn: async (data: { content: string }) => { + await createMemeComment(token, memeId, data.content); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["comments", memeId] }); + queryClient.setQueryData(["memes"], (data) => { + if (!data) return data; + return incrementeCommentCount(data, memeId); + }); + }, + }); +} From 2a9e19af36ea89d4d836a45882be31683954eece Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Sat, 28 Sep 2024 22:47:22 +0200 Subject: [PATCH 06/16] refactor: Remove unused code --- src/components/comment-form.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/comment-form.tsx b/src/components/comment-form.tsx index 9b7f84d..107d042 100644 --- a/src/components/comment-form.tsx +++ b/src/components/comment-form.tsx @@ -1,9 +1,5 @@ import { Avatar, Box, Flex, Input } from "@chakra-ui/react"; -import { useCallback } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; -import { User } from "../types/user"; -import { createMemeComment } from "../api"; -import { useMutation } from "@tanstack/react-query"; import { useSubmitComment } from "../hooks/use-submit-comment"; import { useMyProfile } from "../hooks/use-my-profile"; From 06405551baaac169c967da6e3261493dc2f6d48e Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Sat, 28 Sep 2024 22:56:37 +0200 Subject: [PATCH 07/16] refactor: Add error message in comment form --- src/components/comment-form.tsx | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/comment-form.tsx b/src/components/comment-form.tsx index 107d042..0a04d6b 100644 --- a/src/components/comment-form.tsx +++ b/src/components/comment-form.tsx @@ -1,4 +1,4 @@ -import { Avatar, Box, Flex, Input } from "@chakra-ui/react"; +import { Avatar, Box, Flex, Input, Text } from "@chakra-ui/react"; import { SubmitHandler, useForm } from "react-hook-form"; import { useSubmitComment } from "../hooks/use-submit-comment"; import { useMyProfile } from "../hooks/use-my-profile"; @@ -15,18 +15,29 @@ export const CommentForm: React.FC = ({ memeId }) => { const { register, handleSubmit, - watch, + resetField, + setError, formState: { errors }, } = useForm(); - console.log(",,,memeId", memeId); const { mutate } = useSubmitComment(memeId); const user = useMyProfile(); const onSubmit: SubmitHandler = async (data) => { - console.log("on submit"); - mutate(data); + const trimed = data.content.trim(); + if (trimed.length === 0) { + return; + } + try { + mutate(data); + resetField("content"); + } catch (e) { + setError("content", { + type: "custom", + message: "Oopps! Something went wrong. Try again.", + }); + } }; return ( @@ -46,6 +57,11 @@ export const CommentForm: React.FC = ({ memeId }) => { {...register("content", { required: true })} />
+ {errors.content && ( + + {errors.content.message} + + )} ); From 8362d02d5e3282d99bc3c57ae308136f6117263f Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Sat, 28 Sep 2024 23:48:16 +0200 Subject: [PATCH 08/16] fix: timezone --- src/components/meme-card.tsx | 3 ++- src/components/meme-comment-item.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx index ab16273..1e66269 100644 --- a/src/components/meme-card.tsx +++ b/src/components/meme-card.tsx @@ -11,6 +11,7 @@ type MemeCardrops = { commentsOpened: boolean; onOpenComments: (memeId: string) => void; }; +//TODO: fix timezone from backend export const MemeCard: React.FC = ({ meme, commentsOpened, @@ -24,7 +25,7 @@ export const MemeCard: React.FC = ({ nameTestId={`meme-author-${meme.id}`} /> - {format(meme.createdAt)} + {format(meme.createdAt + "Z")} = ({ memeId, }: MemeCommentProps) => { const { data: author } = useUser(comment.authorId); - + //TODO:fix timezone from backend return ( = ({ - {format(comment.createdAt)} + {format(comment.createdAt + "Z")} Date: Mon, 30 Sep 2024 13:17:59 +0200 Subject: [PATCH 09/16] docs: Add feed code review --- doc/review/meme-feed-code-review.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 doc/review/meme-feed-code-review.md diff --git a/doc/review/meme-feed-code-review.md b/doc/review/meme-feed-code-review.md new file mode 100644 index 0000000..ea4b224 --- /dev/null +++ b/doc/review/meme-feed-code-review.md @@ -0,0 +1,16 @@ +# Meme feed code review report + +## Identified Problems: + +- The entire feed is loaded recursively on the home page. +- The components are not sufficiently broken down. +- The `index.tsx` file of the feed handles API calls, data manipulation, and display at the same time. +- All comments for each meme are loaded from the start. +- The publication time of memes and comments does not take into account the user's timezone. + +## Solutions Provided: + +- Breakdown of the feed into: `MemeListLayout` + `MemeList` (for data control) + `MemeCard` (for display). +- Added `useInView` and `useInfiniteQuery` to load `MemeCard` on scroll. +- Only the first page of comments is preloaded, the rest are loaded on scroll once the comments are opened. +- Added a temporary fix for timezones while awaiting a backend fix: `format(comment.createdAt + "Z")`. From b49174c87cdd633955fbbd1e0b168c087f9e4a18 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Mon, 30 Sep 2024 13:33:54 +0200 Subject: [PATCH 10/16] test: Add click handler in meme card comment --- .../routes/_authentication/index.test.tsx | 77 ++++++++++++++----- src/components/meme-comment-item.tsx | 1 + tests/setup.ts | 16 ++-- 3 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/__tests__/routes/_authentication/index.test.tsx b/src/__tests__/routes/_authentication/index.test.tsx index 1cfde30..f2d943f 100644 --- a/src/__tests__/routes/_authentication/index.test.tsx +++ b/src/__tests__/routes/_authentication/index.test.tsx @@ -1,4 +1,4 @@ -import { screen, waitFor } from "@testing-library/react"; +import { screen, waitFor, fireEvent } from "@testing-library/react"; import { ChakraProvider } from "@chakra-ui/react"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { AuthenticationContext } from "../../../contexts/authentication"; @@ -37,42 +37,77 @@ describe("routes/_authentication/index", () => { await waitFor(() => { // We check that the right author's username is displayed - expect(screen.getByTestId("meme-author-dummy_meme_id_1")).toHaveTextContent('dummy_user_1'); - + expect( + screen.getByTestId("meme-author-dummy_meme_id_1") + ).toHaveTextContent("dummy_user_1"); + + //click in comments + fireEvent.click( + screen.getByTestId("meme-comments-section-dummy_meme_id_1") + ); + // We check that the right meme's picture is displayed expect(screen.getByTestId("meme-picture-dummy_meme_id_1")).toHaveStyle({ - 'background-image': 'url("https://dummy.url/meme/1")', + "background-image": 'url("https://dummy.url/meme/1")', }); // We check that the right texts are displayed at the right positions const text1 = screen.getByTestId("meme-picture-dummy_meme_id_1-text-0"); const text2 = screen.getByTestId("meme-picture-dummy_meme_id_1-text-1"); - expect(text1).toHaveTextContent('dummy text 1'); + expect(text1).toHaveTextContent("dummy text 1"); expect(text1).toHaveStyle({ - 'top': '0px', - 'left': '0px', + top: "0px", + left: "0px", }); - expect(text2).toHaveTextContent('dummy text 2'); + expect(text2).toHaveTextContent("dummy text 2"); expect(text2).toHaveStyle({ - 'top': '100px', - 'left': '100px', + top: "100px", + left: "100px", }); // We check that the right description is displayed - expect(screen.getByTestId("meme-description-dummy_meme_id_1")).toHaveTextContent('dummy meme 1'); - + expect( + screen.getByTestId("meme-description-dummy_meme_id_1") + ).toHaveTextContent("dummy meme 1"); + // We check that the right number of comments is displayed - expect(screen.getByTestId("meme-comments-count-dummy_meme_id_1")).toHaveTextContent('3 comments'); - + expect( + screen.getByTestId("meme-comments-count-dummy_meme_id_1") + ).toHaveTextContent("3 comments"); + // We check that the right comments with the right authors are displayed - expect(screen.getByTestId("meme-comment-content-dummy_meme_id_1-dummy_comment_id_1")).toHaveTextContent('dummy comment 1'); - expect(screen.getByTestId("meme-comment-author-dummy_meme_id_1-dummy_comment_id_1")).toHaveTextContent('dummy_user_1'); + expect( + screen.getByTestId( + "meme-comment-content-dummy_meme_id_1-dummy_comment_id_1" + ) + ).toHaveTextContent("dummy comment 1"); + expect( + screen.getByTestId( + "meme-comment-author-dummy_meme_id_1-dummy_comment_id_1" + ) + ).toHaveTextContent("dummy_user_1"); + + expect( + screen.getByTestId( + "meme-comment-content-dummy_meme_id_1-dummy_comment_id_2" + ) + ).toHaveTextContent("dummy comment 2"); + expect( + screen.getByTestId( + "meme-comment-author-dummy_meme_id_1-dummy_comment_id_2" + ) + ).toHaveTextContent("dummy_user_2"); - expect(screen.getByTestId("meme-comment-content-dummy_meme_id_1-dummy_comment_id_2")).toHaveTextContent('dummy comment 2'); - expect(screen.getByTestId("meme-comment-author-dummy_meme_id_1-dummy_comment_id_2")).toHaveTextContent('dummy_user_2'); - - expect(screen.getByTestId("meme-comment-content-dummy_meme_id_1-dummy_comment_id_3")).toHaveTextContent('dummy comment 3'); - expect(screen.getByTestId("meme-comment-author-dummy_meme_id_1-dummy_comment_id_3")).toHaveTextContent('dummy_user_3'); + expect( + screen.getByTestId( + "meme-comment-content-dummy_meme_id_1-dummy_comment_id_3" + ) + ).toHaveTextContent("dummy comment 3"); + expect( + screen.getByTestId( + "meme-comment-author-dummy_meme_id_1-dummy_comment_id_3" + ) + ).toHaveTextContent("dummy_user_3"); }); }); }); diff --git a/src/components/meme-comment-item.tsx b/src/components/meme-comment-item.tsx index 0b2b49a..328b556 100644 --- a/src/components/meme-comment-item.tsx +++ b/src/components/meme-comment-item.tsx @@ -6,6 +6,7 @@ import { useUser } from "../hooks/use-user"; type MemeCommentProps = { comment: MemeComment; memeId: string; + dataTestId?: string; }; export const MemeCommentItem: React.FC = ({ diff --git a/tests/setup.ts b/tests/setup.ts index f9938d1..1339bcb 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,14 +1,20 @@ import "@testing-library/jest-dom/vitest"; -import { afterEach } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import { setupServer } from 'msw/node'; -import { handlers } from './mocks/handlers'; +import { afterEach, beforeEach } from "vitest"; +import { cleanup } from "@testing-library/react"; +import { setupServer } from "msw/node"; +import { handlers } from "./mocks/handlers"; const server = setupServer(...handlers); beforeAll(() => server.listen()); + +beforeEach(() => { + window.scrollTo = vitest.fn(); +}); + afterEach(() => { server.resetHandlers(); + vitest.clearAllMocks(); cleanup(); }); -afterAll(() => server.close()); \ No newline at end of file +afterAll(() => server.close()); From c9eb661ca143f37f53635551d9fb01f3843955c3 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Mon, 30 Sep 2024 16:23:27 +0200 Subject: [PATCH 11/16] refactor: Migrate to axios + zod --- package-lock.json | 75 +++++++++++++-- package.json | 7 +- src/api.ts | 132 -------------------------- src/components/user-dropdown.tsx | 20 ++-- src/contexts/authentication.tsx | 67 +++++++------ src/hooks/use-comment-list.ts | 6 +- src/hooks/use-feed.ts | 7 +- src/hooks/use-submit-comment.ts | 12 +-- src/hooks/use-user.ts | 6 +- src/routes/_authentication.tsx | 1 + src/routes/login.tsx | 3 +- src/services/api/auth-cookie.ts | 76 +++++++++++++++ src/services/api/axios-client.ts | 44 +++++++++ src/services/api/endpoints/auth.ts | 17 ++++ src/services/api/endpoints/comment.ts | 32 +++++++ src/services/api/endpoints/meme.ts | 35 +++++++ src/services/api/endpoints/user.ts | 15 +++ src/services/api/errors.ts | 23 +++++ src/services/api/index.ts | 5 + src/types/authentication-state.ts | 9 ++ 20 files changed, 392 insertions(+), 200 deletions(-) delete mode 100644 src/api.ts create mode 100644 src/services/api/auth-cookie.ts create mode 100644 src/services/api/axios-client.ts create mode 100644 src/services/api/endpoints/auth.ts create mode 100644 src/services/api/endpoints/comment.ts create mode 100644 src/services/api/endpoints/meme.ts create mode 100644 src/services/api/endpoints/user.ts create mode 100644 src/services/api/errors.ts create mode 100644 src/services/api/index.ts create mode 100644 src/types/authentication-state.ts diff --git a/package-lock.json b/package-lock.json index 5c483d5..76054f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,19 @@ "@phosphor-icons/react": "^2.1.7", "@tanstack/react-query": "^5.51.17", "@tanstack/react-router": "^1.46.0", + "@types/js-cookie": "^3.0.6", + "axios": "^1.7.7", + "eventemitter3": "^5.0.1", "framer-motion": "^11.3.19", + "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.52.1", "react-intersection-observer": "^9.13.1", - "timeago.js": "^4.0.2" + "timeago.js": "^4.0.2", + "zod": "^3.23.8" }, "devDependencies": { "@tanstack/router-devtools": "^1.46.0", @@ -3599,6 +3604,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", @@ -4183,8 +4194,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/attr-accept": { "version": "2.2.2", @@ -4194,6 +4204,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-dead-code-elimination": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.6.tgz", @@ -4541,7 +4562,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4702,7 +4722,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5136,6 +5155,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -5317,6 +5342,26 @@ "node": ">=10" } }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -5337,7 +5382,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6288,6 +6332,15 @@ "node": ">=8" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6565,7 +6618,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -6574,7 +6626,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7167,6 +7218,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -8618,7 +8675,7 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 12c331e..ca5ee71 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,19 @@ "@phosphor-icons/react": "^2.1.7", "@tanstack/react-query": "^5.51.17", "@tanstack/react-router": "^1.46.0", + "@types/js-cookie": "^3.0.6", + "axios": "^1.7.7", + "eventemitter3": "^5.0.1", "framer-motion": "^11.3.19", + "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.3", "react-hook-form": "^7.52.1", "react-intersection-observer": "^9.13.1", - "timeago.js": "^4.0.2" + "timeago.js": "^4.0.2", + "zod": "^3.23.8" }, "devDependencies": { "@tanstack/router-devtools": "^1.46.0", diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index e88bb40..0000000 --- a/src/api.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Meme } from "./types/meme"; -import { MemeComment } from "./types/meme-comment"; -import { PaginationResponse } from "./types/pagination-response"; -import { User } from "./types/user"; - -const BASE_URL = import.meta.env.VITE_API_BASE_URL as string; - -export class UnauthorizedError extends Error { - constructor() { - super("Unauthorized"); - } -} - -export class NotFoundError extends Error { - constructor() { - super("Not Found"); - } -} - -function checkStatus(response: Response) { - if (response.status === 401) { - throw new UnauthorizedError(); - } - if (response.status === 404) { - throw new NotFoundError(); - } - return response; -} - -export type LoginResponse = { - jwt: string; -}; - -/** - * Authenticate the user with the given credentials - * @param username - * @param password - * @returns - */ -export async function login( - username: string, - password: string -): Promise { - return await fetch(`${BASE_URL}/authentication/login`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username, password }), - }).then((res) => checkStatus(res).json()); -} - -/** - * Get a user by their id - * @param token - * @param id - * @returns - */ -export async function getUserById(token: string, id: string): Promise { - return await fetch(`${BASE_URL}/users/${id}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }).then((res) => checkStatus(res).json()); -} - -/** - * Get the list of memes for a given page - * @param token - * @param page - * @returns - */ -export async function getMemes( - token: string, - page: number -): Promise> { - return await fetch(`${BASE_URL}/memes?page=${page}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }).then((res) => checkStatus(res).json()); -} - -/** - * Get comments for a meme - * @param token - * @param memeId - * @returns - */ -export async function getMemeComments( - token: string, - memeId: string, - page: number -): Promise> { - return await fetch(`${BASE_URL}/memes/${memeId}/comments?page=${page}`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }).then((res) => checkStatus(res).json()); -} - -export type CreateCommentResponse = { - id: string; - content: string; - createdAt: string; - authorId: string; - memeId: string; -}; - -/** - * Create a comment for a meme - * @param token - * @param memeId - * @param content - */ -export async function createMemeComment( - token: string, - memeId: string, - content: string -): Promise { - return await fetch(`${BASE_URL}/memes/${memeId}/comments`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ content }), - }).then((res) => checkStatus(res).json()); -} diff --git a/src/components/user-dropdown.tsx b/src/components/user-dropdown.tsx index f4a7cb6..b9f98a6 100644 --- a/src/components/user-dropdown.tsx +++ b/src/components/user-dropdown.tsx @@ -11,15 +11,16 @@ import { import { useQuery } from "@tanstack/react-query"; import { CaretDown, CaretUp, SignOut } from "@phosphor-icons/react"; import { useAuthentication } from "../contexts/authentication"; -import { getUserById } from "../api"; +import { getUserById } from "../services/api"; export const UserDropdown: React.FC = () => { + console.log("render drop down"); const { state, signout } = useAuthentication(); const { data: user, isLoading } = useQuery({ queryKey: ["user", state.isAuthenticated ? state.userId : "anon"], queryFn: () => { if (state.isAuthenticated) { - return getUserById(state.token, state.userId); + return getUserById(state.userId); } return null; }, @@ -43,14 +44,19 @@ export const UserDropdown: React.FC = () => { src={user?.pictureUrl} border="1px solid white" /> - - {user?.username} - - + {user?.username} + - } onClick={signout}>Sign Out + } onClick={signout}> + Sign Out + )} diff --git a/src/contexts/authentication.tsx b/src/contexts/authentication.tsx index 34ebf26..9636de1 100644 --- a/src/contexts/authentication.tsx +++ b/src/contexts/authentication.tsx @@ -4,19 +4,17 @@ import { PropsWithChildren, useCallback, useContext, + useEffect, useMemo, useState, } from "react"; - -export type AuthenticationState = - | { - isAuthenticated: true; - token: string; - userId: string; - } - | { - isAuthenticated: false; - }; +import { + authEventEmitter, + getAuthState, + logout, + setToken, +} from "../services/api"; +import { AuthenticationState } from "../types/authentication-state"; export type Authentication = { state: AuthenticationState; @@ -25,36 +23,49 @@ export type Authentication = { }; export const AuthenticationContext = createContext( - undefined, + undefined ); export const AuthenticationProvider: React.FC = ({ children, }) => { - const [state, setState] = useState({ - isAuthenticated: false, - }); + const [state, setState] = useState(getAuthState()); - const authenticate = useCallback( - (token: string) => { - setState({ - isAuthenticated: true, - token, - userId: jwtDecode<{ id: string }>(token).id, - }); - }, - [setState], - ); + useEffect(() => { + const login = () => { + console.log("handle login event"); + setState(getAuthState()); + }; + + const logout = () => { + console.log("handle logout event"); + setState(getAuthState()); + }; + + authEventEmitter.on("login", login); + authEventEmitter.on("logout", logout); + return () => { + console.log("unmount"); + authEventEmitter.off("login", login); + authEventEmitter.off("logout", logout); + }; + }, []); + + const authenticate = useCallback((jwt: string) => { + setToken(jwt); + }, []); const signout = useCallback(() => { - setState({ isAuthenticated: false }); - }, [setState]); + logout(); + }, []); const contextValue = useMemo( () => ({ state, authenticate, signout }), - [state, authenticate, signout], + [state, authenticate, signout] ); + console.log("rerender auth context"); + return ( {children} @@ -66,7 +77,7 @@ export function useAuthentication() { const context = useContext(AuthenticationContext); if (!context) { throw new Error( - "useAuthentication must be used within an AuthenticationProvider", + "useAuthentication must be used within an AuthenticationProvider" ); } return context; diff --git a/src/hooks/use-comment-list.ts b/src/hooks/use-comment-list.ts index e6bef3b..ad09f7b 100644 --- a/src/hooks/use-comment-list.ts +++ b/src/hooks/use-comment-list.ts @@ -1,14 +1,12 @@ -import { useAuthToken } from "../contexts/authentication"; -import { getMemeComments } from "../api"; import { useInfiniteQuery } from "@tanstack/react-query"; import { getNextPageParam } from "../utils/get-next-page-param"; +import { getMemeComments } from "../services/api"; export function useCommentList(memeId: string) { - const token = useAuthToken(); return useInfiniteQuery({ queryKey: ["comments", memeId], initialPageParam: 1, - queryFn: ({ pageParam }) => getMemeComments(token, memeId, pageParam), + queryFn: ({ pageParam }) => getMemeComments(memeId, pageParam), getNextPageParam, }); } diff --git a/src/hooks/use-feed.ts b/src/hooks/use-feed.ts index eae1d0d..bb639ea 100644 --- a/src/hooks/use-feed.ts +++ b/src/hooks/use-feed.ts @@ -1,15 +1,12 @@ -import { useAuthToken } from "../contexts/authentication"; import { useInfiniteQuery } from "@tanstack/react-query"; -import { getMemes } from "../api"; import { getNextPageParam } from "../utils/get-next-page-param"; +import { getMemes } from "../services/api"; export function useFeed() { - const token = useAuthToken(); - return useInfiniteQuery({ queryKey: ["memes"], initialPageParam: 1, - queryFn: ({ pageParam }) => getMemes(token, pageParam), + queryFn: ({ pageParam }) => getMemes(pageParam), getNextPageParam, }); } diff --git a/src/hooks/use-submit-comment.ts b/src/hooks/use-submit-comment.ts index 4d02d88..3223647 100644 --- a/src/hooks/use-submit-comment.ts +++ b/src/hooks/use-submit-comment.ts @@ -1,12 +1,7 @@ -import { - useInfiniteQuery, - useMutation, - useQueryClient, -} from "@tanstack/react-query"; -import { createMemeComment } from "../api"; -import { useAuthToken } from "../contexts/authentication"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Meme } from "../types/meme"; import { PaginationResponse } from "../types/pagination-response"; +import { createMemeComment } from "../services/api"; type InfiniteData = { pages: PaginationResponse[]; @@ -29,10 +24,9 @@ function incrementeCommentCount(data: InfiniteData, memeId: string) { export function useSubmitComment(memeId: string) { const queryClient = useQueryClient(); - const token = useAuthToken(); return useMutation({ mutationFn: async (data: { content: string }) => { - await createMemeComment(token, memeId, data.content); + await createMemeComment(memeId, data.content); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["comments", memeId] }); diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts index 0150a73..f8cc46e 100644 --- a/src/hooks/use-user.ts +++ b/src/hooks/use-user.ts @@ -1,11 +1,9 @@ import { useQuery } from "@tanstack/react-query"; -import { getUserById } from "../api"; -import { useAuthToken } from "../contexts/authentication"; +import { getUserById } from "../services/api"; export function useUser(userId: string) { - const token = useAuthToken(); return useQuery({ queryKey: ["user", userId], - queryFn: () => getUserById(token, userId), + queryFn: () => getUserById(userId), }); } diff --git a/src/routes/_authentication.tsx b/src/routes/_authentication.tsx index 771e619..22f1fbf 100644 --- a/src/routes/_authentication.tsx +++ b/src/routes/_authentication.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { createFileRoute, Navigate, diff --git a/src/routes/login.tsx b/src/routes/login.tsx index ce21459..83c715f 100644 --- a/src/routes/login.tsx +++ b/src/routes/login.tsx @@ -11,8 +11,9 @@ import { import { useMutation } from "@tanstack/react-query"; import { createFileRoute, Navigate } from "@tanstack/react-router"; import { SubmitHandler, useForm } from "react-hook-form"; -import { login, UnauthorizedError } from "../api"; import { useAuthentication } from "../contexts/authentication"; +import { login } from "../services/api"; +import { UnauthorizedError } from "../services/api/errors"; type SearchParams = { redirect?: string; diff --git a/src/services/api/auth-cookie.ts b/src/services/api/auth-cookie.ts new file mode 100644 index 0000000..ca52e23 --- /dev/null +++ b/src/services/api/auth-cookie.ts @@ -0,0 +1,76 @@ +import Cookies from "js-cookie"; +import { EventEmitter } from "eventemitter3"; +import { jwtDecode } from "jwt-decode"; +import { AuthenticationState } from "../../types/authentication-state"; + +interface AuthEvents { + login: (jwt: string) => void; + logout: () => void; +} + +export const authEventEmitter = new EventEmitter(); + +export function setToken(token: string) { + Cookies.set("token", token, { expires: 7 }); + authEventEmitter.emit("login", token); +} + +export function hasValidToken() { + const token = getToken(); + if (!token) { + return false; + } + const exp = jwtDecode<{ exp: number }>(token).exp; + return exp * 1000 > Date.now(); +} + +export function getToken() { + const token = Cookies.get("token"); + return token; +} + +// const userIdCache = { token: "", userId: "" }; + +const notAuthenticated: AuthenticationState = { isAuthenticated: false }; +export function getAuthState(): AuthenticationState { + try { + const isValid = hasValidToken(); + const token = getToken(); + if (!isValid || !token) { + throw new Error("no token found"); + } + + const userId = jwtDecode<{ id: string }>(token).id; + console.log("userid", userId); + if (!userId) { + throw new Error("no user id found"); + } + + return { + isAuthenticated: true, + token, + userId, + }; + } catch (e) { + return notAuthenticated; + } +} + +// export function getUserId() { +// const token = Cookies.get("token"); +// if (!token) { +// return undefined; +// } +// if (userIdCache.token === token) { +// return userIdCache.userId; +// } +// const userId = jwtDecode<{ id: string }>(token).id; +// userIdCache.token = token; +// userIdCache.userId = userId; +// return userId; +// } + +export function logout() { + Cookies.remove("token"); + authEventEmitter.emit("logout"); +} diff --git a/src/services/api/axios-client.ts b/src/services/api/axios-client.ts new file mode 100644 index 0000000..09f8704 --- /dev/null +++ b/src/services/api/axios-client.ts @@ -0,0 +1,44 @@ +import axios from "axios"; +import { + ApiError, + NotFoundError, + ServerError, + UnauthorizedError, +} from "./errors"; +import { getToken, logout } from "./auth-cookie"; + +const BASE_URL = import.meta.env.VITE_API_BASE_URL as string; + +export const axiosClient = axios.create({ + baseURL: BASE_URL, + timeout: 5000, + headers: { "Content-Type": "application/json" }, +}); + +axiosClient.interceptors.request.use( + (config) => { + const token = getToken(); + if (token) { + config.headers.Authorization = `Bearer ${getToken()}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +axiosClient.interceptors.response.use( + (response) => response, + (error) => { + switch (error.response.status) { + case 401: + logout(); + return Promise.reject(new UnauthorizedError()); + case 404: + return Promise.reject(new NotFoundError()); + case 500: + return Promise.reject(new ServerError()); + default: + return Promise.reject(new ApiError()); + } + } +); diff --git a/src/services/api/endpoints/auth.ts b/src/services/api/endpoints/auth.ts new file mode 100644 index 0000000..47f23a6 --- /dev/null +++ b/src/services/api/endpoints/auth.ts @@ -0,0 +1,17 @@ +import { axiosClient } from "../axios-client"; +import * as z from "zod"; + +const schema = z.object({ + jwt: z.string(), +}); + +export type LoginResponse = z.infer; + +export async function login(username: string, password: string) { + const result = await axiosClient.post("/authentication/login", { + username, + password, + }); + + return schema.parse(result.data); +} diff --git a/src/services/api/endpoints/comment.ts b/src/services/api/endpoints/comment.ts new file mode 100644 index 0000000..280a26c --- /dev/null +++ b/src/services/api/endpoints/comment.ts @@ -0,0 +1,32 @@ +import { axiosClient } from "../axios-client"; +import * as z from "zod"; + +const memeCommentSchema = z.object({ + id: z.string(), + authorId: z.string(), + memeId: z.string(), + content: z.string(), + createdAt: z.string(), +}); + +const schema = z.object({ + results: z.array(memeCommentSchema), + total: z.number(), + pageSize: z.number(), +}); + +export type GetMemeCommentsResponse = z.infer; + +export async function getMemeComments(memeId: string, page: number) { + const result = await axiosClient.get( + `/memes/${memeId}/comments?page=${page}` + ); + return schema.parse(result.data); +} + +export async function createMemeComment(memeId: string, content: string) { + const result = await axiosClient.post(`/memes/${memeId}/comments`, { + content, + }); + return memeCommentSchema.parse(result.data); +} diff --git a/src/services/api/endpoints/meme.ts b/src/services/api/endpoints/meme.ts new file mode 100644 index 0000000..3a83a23 --- /dev/null +++ b/src/services/api/endpoints/meme.ts @@ -0,0 +1,35 @@ +import { axiosClient } from "../axios-client"; +import * as z from "zod"; + +const textSchema = z.object({ + content: z.string(), + x: z.number(), + y: z.number(), +}); + +export type Text = z.infer; + +const memeSchema = z.object({ + id: z.string(), + authorId: z.string(), + pictureUrl: z.string(), + description: z.string(), + commentsCount: z.number(), + createdAt: z.string(), + texts: z.array(textSchema), +}); + +export type Meme = z.infer; + +const schema = z.object({ + results: z.array(memeSchema), + total: z.number(), + pageSize: z.number(), +}); + +export type GetMemePageResponse = z.infer; + +export async function getMemes(page: number) { + const result = await axiosClient.get(`/memes?page=${page}`); + return schema.parse(result.data); +} diff --git a/src/services/api/endpoints/user.ts b/src/services/api/endpoints/user.ts new file mode 100644 index 0000000..fd7b761 --- /dev/null +++ b/src/services/api/endpoints/user.ts @@ -0,0 +1,15 @@ +import { axiosClient } from "../axios-client"; +import * as z from "zod"; + +const schema = z.object({ + id: z.string(), + username: z.string(), + pictureUrl: z.string(), +}); + +export type GetUserResponse = z.infer; + +export async function getUserById(userId: string) { + const result = await axiosClient.get(`/users/${userId}`); + return schema.parse(result.data); +} diff --git a/src/services/api/errors.ts b/src/services/api/errors.ts new file mode 100644 index 0000000..beb3823 --- /dev/null +++ b/src/services/api/errors.ts @@ -0,0 +1,23 @@ +export class UnauthorizedError extends Error { + constructor() { + super("Unauthorized"); + } +} + +export class NotFoundError extends Error { + constructor() { + super("Not Found"); + } +} + +export class ServerError extends Error { + constructor() { + super("Internal server error"); + } +} + +export class ApiError extends Error { + constructor() { + super("Internal error"); + } +} diff --git a/src/services/api/index.ts b/src/services/api/index.ts new file mode 100644 index 0000000..7b35770 --- /dev/null +++ b/src/services/api/index.ts @@ -0,0 +1,5 @@ +export * from "./endpoints/auth"; +export * from "./endpoints/comment"; +export * from "./endpoints/meme"; +export * from "./endpoints/user"; +export * from "./auth-cookie"; diff --git a/src/types/authentication-state.ts b/src/types/authentication-state.ts new file mode 100644 index 0000000..2c75456 --- /dev/null +++ b/src/types/authentication-state.ts @@ -0,0 +1,9 @@ +export type AuthenticationState = + | { + isAuthenticated: true; + token: string; + userId: string; + } + | { + isAuthenticated: false; + }; From 87903bc47a793a6e379fc2966df2eb7f88ea65c1 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Mon, 30 Sep 2024 16:24:39 +0200 Subject: [PATCH 12/16] fix: Remove unused code --- src/contexts/authentication.tsx | 1 - src/routes/__root.tsx | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/contexts/authentication.tsx b/src/contexts/authentication.tsx index 9636de1..4982868 100644 --- a/src/contexts/authentication.tsx +++ b/src/contexts/authentication.tsx @@ -1,4 +1,3 @@ -import { jwtDecode } from "jwt-decode"; import { createContext, PropsWithChildren, diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index ef276aa..02174e5 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { Flex, Heading, @@ -11,12 +12,10 @@ import { Link, Outlet, } from "@tanstack/react-router"; -import { - AuthenticationState, - useAuthentication, -} from "../contexts/authentication"; +import { useAuthentication } from "../contexts/authentication"; import { UserDropdown } from "../components/user-dropdown"; import { Plus } from "@phosphor-icons/react"; +import { AuthenticationState } from "../types/authentication-state"; type RouterContext = { authState: AuthenticationState; From 741bebbe6171c51dc1ba79ccdb979ccc811759a5 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Mon, 30 Sep 2024 16:25:57 +0200 Subject: [PATCH 13/16] test: fix login test --- src/__tests__/routes/login.test.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/__tests__/routes/login.test.tsx b/src/__tests__/routes/login.test.tsx index 5a9d17a..5a6a211 100644 --- a/src/__tests__/routes/login.test.tsx +++ b/src/__tests__/routes/login.test.tsx @@ -2,13 +2,11 @@ import { vi } from "vitest"; import { act, fireEvent, waitFor, screen } from "@testing-library/react"; import { renderWithRouter } from "../utils"; import { LoginPage } from "../../routes/login"; -import { - AuthenticationContext, - AuthenticationState, -} from "../../contexts/authentication"; +import { AuthenticationContext } from "../../contexts/authentication"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ChakraProvider } from "@chakra-ui/react"; import { ListenerFn, RouterEvents } from "@tanstack/react-router"; +import { AuthenticationState } from "../../types/authentication-state"; type RenderLoginPageParams = { authenticate?: (token: string) => void; @@ -99,7 +97,7 @@ describe("routes/login", () => { await waitFor(() => { expect( - screen.getByText(/an unknown error occured, please try again later/i), + screen.getByText(/an unknown error occured, please try again later/i) ).toBeInTheDocument(); }); }); @@ -119,7 +117,7 @@ describe("routes/login", () => { expect(onBeforeNavigateMock).toHaveBeenCalledWith( expect.objectContaining({ toLocation: expect.objectContaining({ pathname: "/" }), - }), + }) ); }); }); @@ -140,7 +138,7 @@ describe("routes/login", () => { expect(onBeforeNavigateMock).toHaveBeenCalledWith( expect.objectContaining({ toLocation: expect.objectContaining({ pathname: "/profile" }), - }), + }) ); }); }); From 39dc9586c0912592d6a92876c13d354fdaab61b1 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Mon, 30 Sep 2024 16:33:57 +0200 Subject: [PATCH 14/16] refactor: remove unused types --- src/components/meme-card.tsx | 2 +- src/components/meme-comment-item.tsx | 3 +-- src/hooks/use-submit-comment.ts | 6 ++---- src/services/api/endpoints/comment.ts | 2 ++ src/types/meme-comment.ts | 7 ------- src/types/meme-text.ts | 5 ----- src/types/meme.ts | 11 ----------- src/types/pagination-response.ts | 5 ----- src/types/user.ts | 5 ----- 9 files changed, 6 insertions(+), 40 deletions(-) delete mode 100644 src/types/meme-comment.ts delete mode 100644 src/types/meme-text.ts delete mode 100644 src/types/meme.ts delete mode 100644 src/types/pagination-response.ts delete mode 100644 src/types/user.ts diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx index 1e66269..5e64590 100644 --- a/src/components/meme-card.tsx +++ b/src/components/meme-card.tsx @@ -2,9 +2,9 @@ import { Box, Flex, Text, VStack } from "@chakra-ui/react"; import { MemePicture } from "./meme-picture"; import { format } from "timeago.js"; -import { Meme } from "../types/meme"; import { MemeAuthor } from "./meme-author"; import { MemeCardComments } from "./meme-card-comments"; +import { Meme } from "../services/api"; type MemeCardrops = { meme: Meme; diff --git a/src/components/meme-comment-item.tsx b/src/components/meme-comment-item.tsx index 328b556..20c146d 100644 --- a/src/components/meme-comment-item.tsx +++ b/src/components/meme-comment-item.tsx @@ -1,12 +1,11 @@ import { Avatar, Box, Flex, Text } from "@chakra-ui/react"; -import { MemeComment } from "../types/meme-comment"; import { format } from "timeago.js"; import { useUser } from "../hooks/use-user"; +import { MemeComment } from "../services/api"; type MemeCommentProps = { comment: MemeComment; memeId: string; - dataTestId?: string; }; export const MemeCommentItem: React.FC = ({ diff --git a/src/hooks/use-submit-comment.ts b/src/hooks/use-submit-comment.ts index 3223647..682e6aa 100644 --- a/src/hooks/use-submit-comment.ts +++ b/src/hooks/use-submit-comment.ts @@ -1,10 +1,8 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { Meme } from "../types/meme"; -import { PaginationResponse } from "../types/pagination-response"; -import { createMemeComment } from "../services/api"; +import { createMemeComment, GetMemePageResponse } from "../services/api"; type InfiniteData = { - pages: PaginationResponse[]; + pages: GetMemePageResponse[]; pageParams: unknown; }; diff --git a/src/services/api/endpoints/comment.ts b/src/services/api/endpoints/comment.ts index 280a26c..7885f2b 100644 --- a/src/services/api/endpoints/comment.ts +++ b/src/services/api/endpoints/comment.ts @@ -9,6 +9,8 @@ const memeCommentSchema = z.object({ createdAt: z.string(), }); +export type MemeComment = z.infer; + const schema = z.object({ results: z.array(memeCommentSchema), total: z.number(), diff --git a/src/types/meme-comment.ts b/src/types/meme-comment.ts deleted file mode 100644 index f4a39b9..0000000 --- a/src/types/meme-comment.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type MemeComment = { - id: string; - authorId: string; - memeId: string; - content: string; - createdAt: string; -}; diff --git a/src/types/meme-text.ts b/src/types/meme-text.ts deleted file mode 100644 index fc97f89..0000000 --- a/src/types/meme-text.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type MemeText = { - content: string; - x: number; - y: number; -}; diff --git a/src/types/meme.ts b/src/types/meme.ts deleted file mode 100644 index a2f1e2f..0000000 --- a/src/types/meme.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MemeText } from "./meme-text"; - -export type Meme = { - id: string; - authorId: string; - pictureUrl: string; - description: string; - commentsCount: number; - texts: MemeText[]; - createdAt: string; -}; diff --git a/src/types/pagination-response.ts b/src/types/pagination-response.ts deleted file mode 100644 index d8214e0..0000000 --- a/src/types/pagination-response.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type PaginationResponse = { - total: number; - pageSize: number; - results: T[]; -}; diff --git a/src/types/user.ts b/src/types/user.ts deleted file mode 100644 index 49c9eed..0000000 --- a/src/types/user.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type User = { - id: string; - username: string; - pictureUrl: string; -}; From d4735716d6e45d534746eeb09fd4f04c131d1b17 Mon Sep 17 00:00:00 2001 From: Jeremie Sellam Date: Tue, 1 Oct 2024 00:19:58 +0200 Subject: [PATCH 15/16] refactor: Rework create page with hook-form --- src/components/meme-card.tsx | 2 +- src/hooks/use-submit-meme.ts | 10 ++ src/routes/__root.tsx | 1 + src/routes/_authentication/create.tsx | 133 ++++++++++++++++++-------- src/services/api/endpoints/meme.ts | 27 ++++++ 5 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 src/hooks/use-submit-meme.ts diff --git a/src/components/meme-card.tsx b/src/components/meme-card.tsx index 5e64590..dd1ae69 100644 --- a/src/components/meme-card.tsx +++ b/src/components/meme-card.tsx @@ -18,7 +18,7 @@ export const MemeCard: React.FC = ({ onOpenComments, }) => { return ( - + { + return await createMeme(data); + }, + }); +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 02174e5..9f93c9e 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -24,6 +24,7 @@ type RouterContext = { export const Route = createRootRouteWithContext()({ component: () => { const { state } = useAuthentication(); + return ( {/* Header */} diff --git a/src/routes/_authentication/create.tsx b/src/routes/_authentication/create.tsx index 8e0a7c5..1f1e8ae 100644 --- a/src/routes/_authentication/create.tsx +++ b/src/routes/_authentication/create.tsx @@ -10,57 +10,100 @@ import { Textarea, VStack, } from "@chakra-ui/react"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, Navigate } from "@tanstack/react-router"; import { MemeEditor } from "../../components/meme-editor"; -import { useMemo, useState } from "react"; -import { MemePictureProps } from "../../components/meme-picture"; +import { useEffect, useMemo } from "react"; import { Plus, Trash } from "@phosphor-icons/react"; +import { + SubmitHandler, + useFieldArray, + useForm, + useWatch, +} from "react-hook-form"; +import { Text } from "../../services/api"; +import { useSubmitMeme } from "../../hooks/use-submit-meme"; export const Route = createFileRoute("/_authentication/create")({ component: CreateMemePage, }); -type Picture = { - url: string; - file: File; +type CreateForm = { + picture: File; + description: string; + texts: Text[]; }; function CreateMemePage() { - const [picture, setPicture] = useState(null); - const [texts, setTexts] = useState([]); + const { + control, + register, + handleSubmit, + setValue, + watch: watchForm, + } = useForm({ + defaultValues: { + texts: [], + description: "", + picture: undefined, + }, + }); + const { fields, append, remove } = useFieldArray({ + control, + name: "texts", + shouldUnregister: true, + }); + + const { mutate, data } = useSubmitMeme(); + + useEffect(() => { + console.log("mutate data", data); + }, [data]); + + const onSubmit: SubmitHandler = async (data) => { + const result = mutate(data); + console.log("result", result); + }; const handleDrop = (file: File) => { - setPicture({ - url: URL.createObjectURL(file), - file, - }); + setValue("picture", file); }; + const watchPicture = watchForm("picture"); + + const pictureUrl = useMemo(() => { + if (!watchPicture) { + return; + } + return URL.createObjectURL(watchPicture); + }, [watchPicture]); + const handleAddCaptionButtonClick = () => { - setTexts([ - ...texts, - { - content: `New caption ${texts.length + 1}`, - x: Math.random() * 400, - y: Math.random() * 225, - }, - ]); + append({ + content: `New caption ${fields.length + 1}`, + x: Math.random() * 400, + y: Math.random() * 225, + }); }; - const handleDeleteCaptionButtonClick = (index: number) => { - setTexts(texts.filter((_, i) => i !== index)); - }; + //TODO: as Text[] should be removed + const watchedTexts = useWatch({ + control, + name: "texts", + }) as Text[]; const memePicture = useMemo(() => { - if (!picture) { + if (!pictureUrl) { return undefined; } - return { - pictureUrl: picture.url, - texts, + pictureUrl: pictureUrl || "", + texts: watchedTexts || [], }; - }, [picture, texts]); + }, [pictureUrl, watchedTexts]); + + if (data) { + return ; + } return ( @@ -76,7 +119,10 @@ function CreateMemePage() { Describe your meme -