diff --git a/.vscode/settings.json b/.vscode/settings.json index ce758c7..fd2040b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "godotenv" + "downvotes", + "godotenv", + "upvotes" ] } \ No newline at end of file diff --git a/enshi/package-lock.json b/enshi/package-lock.json index 641fb3f..2b04d15 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -8,18 +8,24 @@ "name": "enshi", "version": "0.0.0", "dependencies": { - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.7", "html-react-parser": "^5.1.16", "i18n": "^0.15.1", "i18next": "^23.14.0", "i18next-browser-languagedetector": "^8.0.0", + "immer": "^10.1.1", "jotai": "^2.9.3", + "jotai-immer": "^0.4.1", "primereact": "^10.8.2", "quill": "^2.0.2", "react": "^18.3.1", @@ -1227,6 +1233,42 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -1414,25 +1456,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", - "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", @@ -1449,6 +1491,158 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -1619,6 +1813,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -2030,17 +2233,17 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz", - "integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz", + "integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -2060,6 +2263,45 @@ } } }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", @@ -2213,6 +2455,130 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", + "integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", @@ -2504,6 +2870,42 @@ } } }, + "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz", @@ -2540,6 +2942,37 @@ } } }, + "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-scroll-area": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz", + "integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-tooltip": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", @@ -2808,9 +3241,19 @@ ] }, "node_modules/@tanstack/query-core": { - "version": "5.54.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz", - "integrity": "sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==", + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", + "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.59.20.tgz", + "integrity": "sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==", "license": "MIT", "funding": { "type": "github", @@ -2818,12 +3261,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.55.0.tgz", - "integrity": "sha512-2uYuxEbRQD8TORUiTUacEOwt1e8aoSqUOJFGY5TUrh6rQ3U85zrMS2wvbNhBhXGh6Vj69QDCP2yv8tIY7joo6Q==", + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.0.tgz", + "integrity": "sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.54.1" + "@tanstack/query-core": "5.60.6" }, "funding": { "type": "github", @@ -2833,6 +3276,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.61.0.tgz", + "integrity": "sha512-hd3yXl+KV+OGQmAw946qHAFp6DygcXcYN+1ai9idYddx6uEQyCwYk3jyIBOQEUw9uzN5DOGJLBsgd/QcimDQsA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.61.0", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4818,6 +5278,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5043,6 +5513,16 @@ } } }, + "node_modules/jotai-immer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jotai-immer/-/jotai-immer-0.4.1.tgz", + "integrity": "sha512-nQTt1HBKie/5OJDck1qLpV1PeBA6bjJLAczEYAx70PD8R4Mbu7gtexfBUCzJh6W6ecsOfwHksAYAesVth6SN9A==", + "license": "MIT", + "peerDependencies": { + "immer": ">=9.0.0", + "jotai": ">=2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/enshi/package.json b/enshi/package.json index 9047b25..e00dff1 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -10,18 +10,24 @@ "preview": "vite preview" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.7", "html-react-parser": "^5.1.16", "i18n": "^0.15.1", "i18next": "^23.14.0", "i18next-browser-languagedetector": "^8.0.0", + "immer": "^10.1.1", "jotai": "^2.9.3", + "jotai-immer": "^0.4.1", "primereact": "^10.8.2", "quill": "^2.0.2", "react": "^18.3.1", diff --git a/enshi/src/@types/PostTypes.ts b/enshi/src/@types/PostTypes.ts new file mode 100644 index 0000000..9068e49 --- /dev/null +++ b/enshi/src/@types/PostTypes.ts @@ -0,0 +1,8 @@ +export type GetRandomPostsRow = { + post_id: string; + // blog_id: number; + user_id: string; + title: string; + // created_at: Date; +} + diff --git a/enshi/src/types/UserType.ts b/enshi/src/@types/UserType.ts similarity index 72% rename from enshi/src/types/UserType.ts rename to enshi/src/@types/UserType.ts index c48416c..0e20c4d 100644 --- a/enshi/src/types/UserType.ts +++ b/enshi/src/@types/UserType.ts @@ -1,4 +1,5 @@ export type TUser = { username: string; isAdmin: boolean; + id?: string | number; } \ No newline at end of file diff --git a/enshi/src/@types/index.d.ts b/enshi/src/@types/index.d.ts new file mode 100644 index 0000000..6b484a7 --- /dev/null +++ b/enshi/src/@types/index.d.ts @@ -0,0 +1,11 @@ +type TToast = { + title: string; + description?: string; + action?: React.Component; +}; + +type TExistingToast = TToast & { + id: number; + resetFunc: (arg0: boolean) => void; + open: boolean; +}; diff --git a/enshi/src/App.css b/enshi/src/App.css index 5ca1f02..73de149 100644 --- a/enshi/src/App.css +++ b/enshi/src/App.css @@ -19,6 +19,10 @@ text-transform: uppercase; } + * { + font-family: "Times New Roman"; + } + /*! * Quill Editor v1.3.6 * https://quilljs.com/ diff --git a/enshi/src/App.tsx b/enshi/src/App.tsx index f840c5e..29bf349 100644 --- a/enshi/src/App.tsx +++ b/enshi/src/App.tsx @@ -1,21 +1,26 @@ -import "./App.css"; -import "@radix-ui/themes/styles.css"; import { Theme, ThemePanel } from "@radix-ui/themes"; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import "@radix-ui/themes/styles.css"; import { QueryClientProvider } from "@tanstack/react-query"; -import queryClient from "./api/QueryClient/QueryClient"; -import { routes } from "./routes/routes"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import "axios"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import queryClient from "./api/QueryClient/QueryClient"; +import "./App.css"; +import ToastProvider from "./Components/ToastProvider/ToastProvider"; +import { routes } from "./routes/routes"; const router = createBrowserRouter(routes); export default function App() { return ( - - - - + + + + + + + ); } diff --git a/enshi/src/AtomStore/AtomStore.ts b/enshi/src/AtomStore/AtomStore.ts index 352eca9..ed53496 100644 --- a/enshi/src/AtomStore/AtomStore.ts +++ b/enshi/src/AtomStore/AtomStore.ts @@ -1,4 +1,50 @@ import { atom } from "jotai"; -import { TUser } from "../types/UserType"; +import { atomWithStorage } from "jotai/utils"; +import { TUser } from "../@types/UserType"; -export const userAtom = atom() \ No newline at end of file +export const userAtom = atom(); + +export const postCreationAtom = atom(); +export const postCreationTitleAtom = atom(); + +type TPostData = { + title: string; + content: string; +}; + +export const storagePostAtom = atomWithStorage( + "draft-post", + { title: "", content: "" }, + { + getItem: (key) => sessionStorage.getItem(key) as any, + setItem: (key, value) => sessionStorage.setItem(key, value as any), + removeItem: (key) => sessionStorage.removeItem(key), + }, + { getOnInit: true } +); + +export const toastAtom = atom([]); +export const setToastAtom = atom(null, (get, set, value: TToast) => { + let maxToastId = Math.max(...get(toastAtom).map((toast) => toast.id)); + maxToastId = maxToastId >= 0 ? maxToastId : 1; + let atomValueWithNewToast = get(toastAtom); + atomValueWithNewToast = [ + ...atomValueWithNewToast, + { + id: maxToastId + 1, + resetFunc: (_) => { + let currentToasts = get(toastAtom); + let afterRemoval = currentToasts.filter( + (toast) => toast.id != maxToastId + 1 + ); + set(toastAtom, afterRemoval); + }, + title: value.title, + action: value.action, + description: value.description, + open: true, + }, + ]; + + set(toastAtom, atomValueWithNewToast); +}); diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index ac30298..9cc5024 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,13 +1,146 @@ -import { Container } from "@radix-ui/themes"; -import React from "react"; +import * as Dialog from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { + Box, + Button, + Container, + Flex, + Select, + Separator, + Text, +} from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { Interweave } from "interweave"; +import { useAtomValue } from "jotai"; +import { useParams } from "react-router-dom"; +import { axiosLocalhost } from "../../api/axios/axios"; +import { userAtom } from "../../AtomStore/AtomStore"; +import ChangePostButton from "./ChangePostButton/ChangePostButton"; +import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader"; +import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton"; +import VoteCounter from "./VoteCounter/VoteCounter"; + +type TArticleViewer = { + htmlToParse?: string; +}; + +export default function ArticleViewer(props: TArticleViewer) { + let queryParams = useParams(); + const user = useAtomValue(userAtom); + + const { data, isPending } = useQuery({ + queryKey: [`post_${queryParams["postId"]}`], + queryFn: async () => { + const response = await axiosLocalhost.get( + `posts/${queryParams["postId"]}` + ); + + return response.data; + }, + gcTime: 0, + refetchOnMount: true, + }); + + if (isPending) return ; -export default function ArticleViewer() { return ( <> -
- - -
+ +
+ + + + {data.title} + + + + + + + + + + + + + + + + + + + + + Add this post to blog + + + + + {`Add "${data.title}" to blog...`} + + + + + + + This + + + This is + updated blog + + + This another + + + + + + + +
+ + + +
+ + + +
+
+
+
+
+ + +
+
+
); } diff --git a/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx b/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx new file mode 100644 index 0000000..f76cc5e --- /dev/null +++ b/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx @@ -0,0 +1,21 @@ +import { Button } from "@radix-ui/themes"; +import { useNavigate } from "react-router-dom"; + +type TChangePostButton = { + postId: number | string; +}; + +export default function ChangePostButton(props: TChangePostButton) { + const navigate = useNavigate(); + + return ( + + ); +} diff --git a/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx new file mode 100644 index 0000000..efa6b31 --- /dev/null +++ b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx @@ -0,0 +1,27 @@ +import { Container, Skeleton, Text } from "@radix-ui/themes"; +import { + headerLong, + headerShort, + pText, +} from "../../../constants/textForSkeleton"; + +export default function SkeletonPostLoader() { + return ( + + + {headerLong} +
+ {headerShort} +
+
+ {pText} +
+
+ {pText} +
+
+ {pText} +
+
+ ); +} diff --git a/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx new file mode 100644 index 0000000..46a880b --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx @@ -0,0 +1,65 @@ +import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons"; +import { IconButton } from "@radix-ui/themes"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +export const UPVOTE = true; +export const DOWNVOTE = false; + +type TVoteButton = { + postId: string; + vote: boolean; +}; + +export default function VoteButton(props: TVoteButton) { + const queryClient = useQueryClient(); + + const { data } = useQuery({ + queryKey: [props.vote + "voteCheck"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-vote/${props.postId}` + ); + + return (response.data?.vote as boolean) === props.vote || false; + }, + gcTime: 0, + }); + + const voteMutation = useMutation({ + mutationKey: [`voteMutation${props.vote}`], + onMutate: async () => { + queryClient.cancelQueries({ queryKey: [props.vote + "voteCheck"] }); + + queryClient.setQueryData([props.vote + "voteCheck"], true); + queryClient.setQueryData([!props.vote + "voteCheck"], false); + }, + mutationFn: async () => { + await axiosLocalhost.post(`post-votes/${props.postId}`, { + vote: props.vote, + }); + }, + onSuccess: () => {}, + onError: () => { + queryClient.setQueryData([props.vote + "voteCheck"], false); + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: [props.vote + "voteCheck"], + }); + queryClient.invalidateQueries({ + queryKey: ["post_vote_counter"], + }); + }, + }); + + return ( + voteMutation.mutate()} + > + {props.vote ? : } + + ); +} diff --git a/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx new file mode 100644 index 0000000..4d54842 --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx @@ -0,0 +1,33 @@ +import { Box, Skeleton } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +type TVoteCounter = { + postId: string; +}; + +export default function VoteCounter(props: TVoteCounter) { + const { data, isLoading } = useQuery({ + queryKey: ["post_vote_counter"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-votes/${props.postId}` + ); + return response.data as { upvotes: number; downvotes: number }; + }, + }); + + const calculateRating = (upvotes: number, downvotes: number) => { + return upvotes + (-downvotes) + } + + if (isLoading) { + return + {calculateRating(0, 0)} + + } + + return + {calculateRating(data?.upvotes || 0, data?.downvotes || 0)} + ; +} diff --git a/enshi/src/Components/BlogBox/BlogBox.tsx b/enshi/src/Components/BlogBox/BlogBox.tsx new file mode 100644 index 0000000..ff5788a --- /dev/null +++ b/enshi/src/Components/BlogBox/BlogBox.tsx @@ -0,0 +1,25 @@ +import { Avatar, Card, Flex, Heading } from "@radix-ui/themes"; +import { useNavigate } from "react-router-dom"; +import UserNicknameLink from "../UserNicknameLink/UserNicknameLink"; + +type TBlogBox = { + title?: string; + blogId?: string; + userId: string; +}; + +export default function BlogBox(props: TBlogBox) { + const navigate = useNavigate(); + + return ( + navigate(``)}> + + {props?.title || "...No title..."} + + + + + + + ); +} diff --git a/enshi/src/Components/Editor/Editor.tsx b/enshi/src/Components/Editor/Editor.tsx index d9e01a7..dd5354d 100644 --- a/enshi/src/Components/Editor/Editor.tsx +++ b/enshi/src/Components/Editor/Editor.tsx @@ -1,25 +1,19 @@ -import Quill, { Delta, } from "quill/core"; -import ReactQuill from "react-quill"; -import React, { - forwardRef, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; import Sources from "quill"; +import Quill, { Delta } from "quill/core"; +import { forwardRef, useEffect, useRef, useState } from "react"; +import ReactQuill from "react-quill"; type TEditor = { readOnly?: boolean; defaultValue?: string | Delta; - onChange: (d: string) => void; // TODO: make type - onSelectionChange?: any; // TODO same as before + onChange?: (d: string) => void; + onSelectionChange?: any; }; const modules = { toolbar: [ - [{ header: [1, 2, 3, false] }], - ["bold", "italic", "underline", "strike", "blockquote"], + [{ header: [1, 2, 3, 4, 5, false] }], + ["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"], [ { list: "ordered" }, { list: "bullet" }, @@ -38,7 +32,9 @@ const modules = { const Editor = forwardRef((props: TEditor) => { const editor = useRef(null); const [quill, setQuill] = useState(null); - const [value, setValue] = useState(new Delta()) + const [value, setValue] = useState(new Delta()); + + const [loaded, setLoaded] = useState(false); useEffect(() => { if (editor.current) { @@ -51,13 +47,33 @@ const Editor = forwardRef((props: TEditor) => { }; }, [editor.current]); - const changeHandler = (val: string, _changeDelta: Delta, _source: Sources, _editor: ReactQuill.UnprivilegedEditor) => { + useEffect(() => { + const quill = new Quill(document.createElement("div")); + const t = quill.clipboard.convert({ + html: props.defaultValue as string, + }) as Delta; + + if (!loaded) { + setValue(t); + + console.log(t); + } + + setLoaded(true); + }, [props.defaultValue]); + + const changeHandler = ( + val: string, + _changeDelta: Delta, + _source: Sources, + _editor: ReactQuill.UnprivilegedEditor + ) => { console.log(val); - console.log(JSON.stringify(quill?.getContents().ops, null, 2)) - let fullDelta = quill?.getContents() - props.onChange(val || "") - setValue(fullDelta || new Delta()) - } + console.log(JSON.stringify(quill?.getContents().ops, null, 2)); + let fullDelta = quill?.getContents(); + if (props.onChange) props.onChange(val || ""); + if (loaded) setValue(fullDelta || new Delta()); + }; return (
@@ -65,11 +81,7 @@ const Editor = forwardRef((props: TEditor) => { value={value} ref={editor} modules={modules} - - onChange={changeHandler} - - theme="snow" placeholder="Type your thoughts here..." /> diff --git a/enshi/src/Components/NavBar/NavBar.tsx b/enshi/src/Components/NavBar/NavBar.tsx index bd60301..17472c6 100644 --- a/enshi/src/Components/NavBar/NavBar.tsx +++ b/enshi/src/Components/NavBar/NavBar.tsx @@ -1,20 +1,15 @@ -import { - Container, -} from "@radix-ui/themes"; -import SearchField from "./SearchField/SearchField"; -import UserButton from "./UserButton/UserButton"; import CustomNavigationMenu from "./NavigationMenu/NavigationMenu"; +import RightButtonBar from "./RightButtonBar/RightButtonBar"; +import SearchField from "./SearchField/SearchField"; export default function NavBar() { return ( - - ); -} \ No newline at end of file +} diff --git a/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx b/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx index e82d496..56e8b9a 100644 --- a/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx +++ b/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx @@ -1,15 +1,19 @@ import * as NavigationMenu from "@radix-ui/react-navigation-menu"; -import { useThemeContext, Button, Heading } from "@radix-ui/themes"; -import { useNavigate, useLocation } from "react-router-dom"; +import { Button, Heading, useThemeContext } from "@radix-ui/themes"; +import { useTranslation } from "react-i18next"; +import { useLocation, useNavigate } from "react-router-dom"; export default function CustomNavigationMenu() { + + const {t} = useTranslation() + return (
- + - +
diff --git a/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx new file mode 100644 index 0000000..eaaaaa6 --- /dev/null +++ b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx @@ -0,0 +1,17 @@ +import { PlusIcon } from "@radix-ui/react-icons"; +import { Button, Text } from "@radix-ui/themes"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +export default function CreatePostButton() { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx b/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx new file mode 100644 index 0000000..3c1ff38 --- /dev/null +++ b/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx @@ -0,0 +1,12 @@ +import CreatePostButton from "./CreatePostButton/CreatePostButton"; +import UserButton from "./UserButton/UserButton"; + + +export default function RightButtonBar() { + return ( +
+ + +
+ ) +} \ No newline at end of file diff --git a/enshi/src/Components/NavBar/UserButton/UserButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx similarity index 66% rename from enshi/src/Components/NavBar/UserButton/UserButton.tsx rename to enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx index c9c1868..ce9ca61 100644 --- a/enshi/src/Components/NavBar/UserButton/UserButton.tsx +++ b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx @@ -7,14 +7,16 @@ import { import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes"; import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js"; import { useAtomValue } from "jotai"; -import { userAtom } from "../../../AtomStore/AtomStore"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { userAtom } from "../../../../AtomStore/AtomStore"; export default function UserButton() { const user = useAtomValue(userAtom); + const { t } = useTranslation(); return ( -
+
@@ -24,24 +26,26 @@ export default function UserButton() { - + - Profile + {t("profile")} - - - - - Your blogs - + + + + + + {t("yourBlogs")} + + @@ -52,15 +56,17 @@ export default function UserButton() { - Log out + {t("signOut")} ) : ( - - - - - Log in - + + + + + + {t("signIn")} + + )} diff --git a/enshi/src/Components/NavBar/SearchField/SearchField.tsx b/enshi/src/Components/NavBar/SearchField/SearchField.tsx index 74cf6fb..02dbf95 100644 --- a/enshi/src/Components/NavBar/SearchField/SearchField.tsx +++ b/enshi/src/Components/NavBar/SearchField/SearchField.tsx @@ -6,9 +6,9 @@ export default function SearchField() { const {t} = useTranslation() return ( -
+
diff --git a/enshi/src/Components/ToastProvider/ToastProvider.tsx b/enshi/src/Components/ToastProvider/ToastProvider.tsx new file mode 100644 index 0000000..51ec9a7 --- /dev/null +++ b/enshi/src/Components/ToastProvider/ToastProvider.tsx @@ -0,0 +1,44 @@ +import { Cross1Icon } from "@radix-ui/react-icons"; +import * as Toast from "@radix-ui/react-toast"; +import { Card, Text } from "@radix-ui/themes"; +import { useAtomValue } from "jotai"; +import React from "react"; +import { toastAtom } from "../../AtomStore/AtomStore"; + +export default function ToastProvider(props: React.PropsWithChildren) { + const toastsToRender = useAtomValue(toastAtom); + + return ( + + {props.children} + + {toastsToRender.map((toast) => { + return ( + + + + + {toast.title} + + + + {toast.description} + + + + + + + ); + })} + + + + ); +} diff --git a/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx b/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx new file mode 100644 index 0000000..05091f5 --- /dev/null +++ b/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx @@ -0,0 +1,33 @@ +import { Skeleton, Text } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { Link } from "react-router-dom"; +import { axiosLocalhost } from "../../api/axios/axios"; + +type TUserNicknameLink = { + userId: string; +}; + +export default function UserNicknameLink(props: TUserNicknameLink) { + const { data, isPending } = useQuery({ + queryKey: [`userLink${props.userId}`], + queryFn: async () => { + const response = await axiosLocalhost.get( + `/user/${props.userId || 0}` + ); + return response.data as string; + }, + }); + + if (isPending) + return ( + + @Nickname + + ); + + return ( + + @{data} + + ); +} diff --git a/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx new file mode 100644 index 0000000..3418d83 --- /dev/null +++ b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx @@ -0,0 +1,22 @@ +import { Container, Text } from "@radix-ui/themes"; +import { t } from "i18next"; +import { useAtomValue } from "jotai"; +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { userAtom } from "../../AtomStore/AtomStore"; + +export default function AuthPageWrapper(props: React.PropsWithChildren) { + const user = useAtomValue(userAtom); + const navigate = useNavigate(); + + if (!user) { + navigate("/login"); + return ( + + {t("errors.unauthorized")} + + ); + } + + return props.children; +} diff --git a/enshi/src/Pages/BlogPage/BlogPage.tsx b/enshi/src/Pages/BlogPage/BlogPage.tsx new file mode 100644 index 0000000..fc1ae4a --- /dev/null +++ b/enshi/src/Pages/BlogPage/BlogPage.tsx @@ -0,0 +1,13 @@ +import { Box } from '@radix-ui/themes' +import { useParams } from 'react-router-dom' + + +export default function BlogPage() { + const queryParams = useParams() + + return ( + + + + ) +} diff --git a/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx new file mode 100644 index 0000000..2b1e3db --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx @@ -0,0 +1,182 @@ +import * as Form from "@radix-ui/react-form"; +import { CrossCircledIcon } from "@radix-ui/react-icons"; +import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes"; +import { useMutation } from "@tanstack/react-query"; +import { t } from "i18next"; +import { useAtom } from "jotai"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import { userAtom } from "../../../AtomStore/AtomStore"; +import UseCapsLock from "../../../hooks/useCapsLock"; +import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton"; + +type TLoginData = { + username: string; + password: string; +}; + +export default function LoginPage() { + const [userAtomValue, setUserAtom] = useAtom(userAtom); + const [showPassword, setShowPassword] = useState(false); + const { isCapsLockOn } = UseCapsLock(); + const [isError, setIsError] = useState(false); + + const navigate = useNavigate(); + + const logInMutation = useMutation({ + mutationFn: async (data: TLoginData) => { + let response = await axiosLocalhost.post( + "/login", + JSON.stringify(data) + ); + setUserAtom({ + username: response.data.username, + isAdmin: false, + id: response.data.id, + }); + }, + + onError: (error, _variables, _context) => { + console.log(error); + setIsError(true); + }, + + onSuccess: () => { + let isAdminFunc = async () => { + let response = await axiosLocalhost.get("/admin/check"); + if (response.status === 200) { + setUserAtom({ + username: userAtomValue?.username || "", + isAdmin: true, + id: userAtomValue?.id, + }); + } + }; + + isAdminFunc(); + + navigate("/"); + }, + }); + + return ( + + + {t("loginForm")} + + { + e.preventDefault(); + let formData = new FormData( + document.querySelector("form") as HTMLFormElement + ); + + let loginData: TLoginData = { + password: (formData.get("password") as string) || "", + username: (formData.get("username") as string) || "", + }; + + logInMutation.mutate(loginData); + }} + > + +
+ + {t("username")} + + + {t("errors.enterUsername")} + +
+ + + + {(validity) => ( + + + + )} + + + +
+ + +
+ + {t("password")} + + + {t("errors.enterPassword")} + +
+ + + + {(validity) => ( + + + + )} + + + + +
+ + + + + + + + + {t("suggestRegister")}{" "} + + {t("register")} + {" "} + {t("now")} + +
+
+ ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx new file mode 100644 index 0000000..50d1e9b --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx @@ -0,0 +1,76 @@ +import { Box, Container, Flex, Spinner } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import Editor from "../../../Components/Editor/Editor"; +import SubmitChangesButton from "./SubmitChangesButton/SubmitChangesButton"; + +export default function PostRedactor() { + const [contentValue, setContentValue] = useState(""); + const [titleValue, setTitleValue] = useState(""); + + const queryParams = useParams(); + + const { isPending } = useQuery({ + queryKey: ["changePostKey", queryParams.postId], + queryFn: async () => { + try { + const response = await axiosLocalhost.get( + `/posts/${queryParams.postId}` + ); + + setTitleValue(response.data["title"]); + setContentValue(response.data["content"]); + + return response.data; + } catch (error) { + console.log(error); + + return error; + } + }, + gcTime: 0, + refetchOnMount: true + }); + + return ( + <> + + + + { + setTitleValue(e.target.value); + }} + value={titleValue} + /> + + + + {isPending ? ( + + ) : ( + + )} + + + + + + + + + ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx new file mode 100644 index 0000000..070448d --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx @@ -0,0 +1,60 @@ +import { Button } from "@radix-ui/themes"; +import { useMutation } from "@tanstack/react-query"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { axiosLocalhost } from "../../../../api/axios/axios"; +import useToast from "../../../../hooks/useToast"; + +type TSubmitChangesButton = { + className: string; + titleValue: string; + contentValue: string; +}; + +export default function SubmitChangesButton(props: TSubmitChangesButton) { + const { t } = useTranslation(); + + const [isDisabled, setIsDisabled] = useState(false); + const createToast = useToast(); + + const navigate = useNavigate(); + const queryParams = useParams(); + + const postMutation = useMutation({ + mutationFn: async () => { + if (!props.titleValue) throw new Error("no title provided"); + if (!props.contentValue || props.contentValue === "


") + throw new Error("no content provided"); + + axiosLocalhost.put(`/posts/${queryParams["postId"]}`, { + title: props.titleValue, + content: props.contentValue, + }); + }, + onMutate: () => { + setIsDisabled(true); + }, + onError: () => { + setIsDisabled(false); + }, + onSuccess: () => { + createToast({title: "Post has been changed!"}) + navigate("/"); + }, + }); + + return ( + + ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx new file mode 100644 index 0000000..4702221 --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx @@ -0,0 +1,265 @@ +import * as Form from "@radix-ui/react-form"; +import { CrossCircledIcon } from "@radix-ui/react-icons"; +import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes"; +import { useMutation } from "@tanstack/react-query"; +import { useSetAtom } from "jotai"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import { userAtom } from "../../../AtomStore/AtomStore"; +import UseCapsLock from "../../../hooks/useCapsLock"; +import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton"; + +type TRegisterData = { + username: string; + password: string; + email: string; +}; + +export default function RegisterPage() { + const setUserAtom = useSetAtom(userAtom) + const [showPassword, setShowPassword] = useState(false); + const [showConfPassword, setShowConfPassword] = useState(false); + const { isCapsLockOn } = UseCapsLock(); + + const { t } = useTranslation(); + + const [isError, setIsError] = useState(false); + + const navigate = useNavigate(); + + const registerMutation = useMutation({ + mutationFn: async (data: TRegisterData) => { + let response = await axiosLocalhost.post("/users", JSON.stringify(data)); + setUserAtom({ + username: response.data.username, + isAdmin: false, + id: response.data.id, + }) + }, + + onError: (error, _variables, _context) => { + console.log(error); + setIsError(true); + }, + + onSuccess: () => { + navigate("/"); + }, + }); + + return ( + + + {t("registerForm")} + + { + e.preventDefault(); + let formData = new FormData( + document.querySelector("form") as HTMLFormElement + ); + + let registerData: TRegisterData = { + password: (formData.get("password") as string) || "", + username: (formData.get("username") as string) || "", + email: (formData.get("email") as string) || "", + }; + + registerMutation.mutate(registerData); + }} + > + +
+ + {t("username")} + + + {t("errors.enterUsername")} + +
+ + + + {(validity) => ( + + + + )} + + + +
+ + +
+ + {t("email")} + + + {t("errors.enterEmail")} + + + {t("errors.invalidEmail")} + +
+ + + + {(validity) => ( + + + + )} + + + +
+ + +
+ + {t("password")} + + + {t("errors.enterPassword")} + +
+ + + + {(validity) => ( + + + + )} + + + + +
+ + +
+ + {t("confirmPassword")} + + + {t("errors.enterPassword")} + + + value !== formData.get("password") + } + > + + {t("errors.passwordsMismatch")} + + +
+ + + + {(validity) => ( + + + + )} + + + + +
+ + + + + + + + + {t("alreadyRegistered")}{" "} + + {t("logIn")} + {" "} + {t("now")} + + + + {t("byPressingTheButton")}{" "} + + {t("termsOfService")}. + + +
+
+ ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx b/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx new file mode 100644 index 0000000..e7bd092 --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx @@ -0,0 +1,40 @@ +import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons"; +import { IconButton, Tooltip } from "@radix-ui/themes"; +import { Dispatch, SetStateAction } from "react"; + +type TShowPasswordButton = { + isShown: boolean; + setIsShown: Dispatch>; +}; + +export default function ShowPasswordButton({ isShown, setIsShown }: TShowPasswordButton) { + return ( +
+ + {isShown ? ( + { + setIsShown(!isShown); + }} + size={"1"} + className="rounded-full" + variant="soft" + > + + + ) : ( + setIsShown(!isShown)} + size={"1"} + className="rounded-full" + variant="soft" + > + + + )} + +
+ ); +} \ No newline at end of file diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx deleted file mode 100644 index 905549a..0000000 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; -import { Outlet } from "react-router-dom"; -import NavBar from "../../Components/NavBar/NavBar"; -import { axiosLocalhost } from "../../api/axios/axios"; -import { Container } from "@radix-ui/themes"; - -export default function MainPage() { - return ( - <> - - - - - ); -} diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx new file mode 100644 index 0000000..eb677ba --- /dev/null +++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx @@ -0,0 +1,43 @@ +import { Box, Container, Flex } from "@radix-ui/themes"; +import { useAtom, useSetAtom } from "jotai"; +import { + postCreationAtom, + postCreationTitleAtom +} from "../../AtomStore/AtomStore"; +import Editor from "../../Components/Editor/Editor"; +import SubmitPostButton from "./SubmitPostButton/SubmitPostButton"; + +export default function PostCreatorPage() { + const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom); + const setContentValue = useSetAtom(postCreationAtom); + + return ( + <> + + + + + { + setTitleValue(e.target.value); + }} + value={titleValue} + /> + + + + + + + + + + + + + ); +} diff --git a/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx new file mode 100644 index 0000000..201d22d --- /dev/null +++ b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx @@ -0,0 +1,64 @@ +import { Button } from "@radix-ui/themes"; +import { useMutation } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import { + postCreationAtom, + postCreationTitleAtom, +} from "../../../AtomStore/AtomStore"; + +type TSubmitPostButton = { + className: string; +}; + +export default function SubmitPostButton(props: TSubmitPostButton) { + const { t } = useTranslation(); + + const [isDisabled, setIsDisabled] = useState(false); + + const [contentValue, setContentValue] = useAtom(postCreationAtom); + const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom); + + const navigate = useNavigate(); + + const postMutation = useMutation({ + mutationFn: async () => { + if (!titleValue) throw new Error("no title provided"); + if (!contentValue || contentValue === "


") + throw new Error("no content provided"); + + axiosLocalhost.post("/posts", { + title: titleValue, + content: contentValue, + }); + }, + onMutate: () => { + setIsDisabled(true); + }, + onError: () => { + setIsDisabled(false); + }, + onSuccess: () => { + setContentValue(""); + setTitleValue(""); + navigate("/"); + }, + }); + + return ( + + ); +} diff --git a/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx b/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx new file mode 100644 index 0000000..2b29f64 --- /dev/null +++ b/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx @@ -0,0 +1,30 @@ +import { ImageIcon } from "@radix-ui/react-icons"; +import { Box, Card, Heading } from "@radix-ui/themes"; +import { useNavigate } from "react-router-dom"; +import { GetRandomPostsRow } from "../../../@types/PostTypes"; + +type TPostCard = { + post: GetRandomPostsRow; +}; + +export default function PostCard({ post }: TPostCard) { + const navigate = useNavigate() + + const clickHandler = () => { + navigate(`/posts/${post.post_id.toString()}`) + } + + return ( + + + + + + + + {post.title} + + + + ); +} diff --git a/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx new file mode 100644 index 0000000..605e04f --- /dev/null +++ b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx @@ -0,0 +1,67 @@ +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Container, Flex, Heading, Separator } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { GetRandomPostsRow } from "../../@types/PostTypes"; +import { axiosLocalhost } from "../../api/axios/axios"; +import PostCard from "./PostCard/PostCard"; + +const LIMIT = 10; + +export default function RandomPostsPage() { + const {t} = useTranslation() + + const { data, refetch } = useQuery({ + queryKey: ["random_posts_key"], + queryFn: async () => { + try { + const response = await axiosLocalhost.get( + `/posts/random?limit=${LIMIT}` + ); + + return response.data as GetRandomPostsRow[]; + } catch (error) { + console.log(`Something went wrong`); + } + + return []; + }, + }); + + return ( + <> + + + {t("discover")} + + + + + + + {data?.map((post, i) => { + return ( + + + + ); + })} + + + + + {/* + + */} + {/* */} + + + + ); +} diff --git a/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx b/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx new file mode 100644 index 0000000..2419e98 --- /dev/null +++ b/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx @@ -0,0 +1,17 @@ +import { Box, Skeleton } from "@radix-ui/themes"; + +export default function SkeletonBoxes() { + return ( + <> + + + + + + + + + + + ); +} diff --git a/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx new file mode 100644 index 0000000..599ae6d --- /dev/null +++ b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx @@ -0,0 +1,126 @@ +import * as Dialog from "@radix-ui/react-dialog"; +import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons"; +import { + Box, + Button, + Container, + Flex, + Separator, + Text, +} from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../api/axios/axios"; +import BlogBox from "../../Components/BlogBox/BlogBox"; +import { JSONWithInt64 } from "../../utils/idnex"; +import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes"; + +export default function UserBlogsPage() { + const { data, isPending, isFetching } = useQuery({ + queryKey: ["userBlogs"], + queryFn: async () => { + const response = await axiosLocalhost.get("/user/blogs", { + transformResponse: [(data) => data], + }); + + let temp = JSONWithInt64(response.data); + + return temp as any[]; + }, + }); + + if (isPending) + return ( + + + + ); + + return ( + + + + + Your blogs + + + + + {data + ? data?.map((blog: any, b) => { + return ( + <> + + + ); + }) + : null} + + + + + + + + + + Create blog + + + Create your new blog. + +
+ + +
+
+ +