From 397d2bcf70c07aad5fc443f7864eea65895ea0b2 Mon Sep 17 00:00:00 2001 From: nihonium Date: Wed, 19 Nov 2025 10:54:52 +0300 Subject: [PATCH 1/2] feat: /titles page implementation with cursor pagination --- modules/frontend/package-lock.json | 651 ++++++++++++++++-- modules/frontend/package.json | 5 +- modules/frontend/src/App.tsx | 2 +- modules/frontend/src/api/index.ts | 9 +- .../cursor.ts => api/models/CursorObj.ts} | 6 +- .../frontend/src/api/models/ReleaseSeason.ts | 7 +- .../Tags.ts => api/models/TitleSort.ts} | 5 +- .../frontend/src/api/models/TitleStatus.ts | 6 +- modules/frontend/src/api/models/User.ts | 4 +- .../src/api/models/UserTitleStatus.ts | 7 +- .../Title.ts => api/models/title_sort.ts} | 3 +- .../src/api/services/DefaultService.ts | 48 +- modules/frontend/src/api_/core/ApiError.ts | 25 - .../src/api_/core/ApiRequestOptions.ts | 17 - modules/frontend/src/api_/core/ApiResult.ts | 11 - .../src/api_/core/CancelablePromise.ts | 131 ---- modules/frontend/src/api_/core/OpenAPI.ts | 32 - modules/frontend/src/api_/core/request.ts | 323 --------- modules/frontend/src/api_/index.ts | 23 - modules/frontend/src/api_/models/Image.ts | 10 - .../frontend/src/api_/models/ReleaseSeason.ts | 13 - modules/frontend/src/api_/models/Review.ts | 5 - modules/frontend/src/api_/models/Studio.ts | 12 - modules/frontend/src/api_/models/Tag.ts | 8 - .../frontend/src/api_/models/TitleStatus.ts | 12 - modules/frontend/src/api_/models/User.ts | 35 - modules/frontend/src/api_/models/UserTitle.ts | 5 - .../src/api_/models/UserTitleStatus.ts | 13 - .../src/api_/services/DefaultService.ts | 148 ---- .../src/components/ListView/ListView.tsx | 133 ++-- .../src/components/ListView/useListView.tsx | 37 - .../components/cards/TitleCardHorizontal.tsx | 10 +- .../src/components/cards/TitleCardSquare.tsx | 8 +- modules/frontend/src/index.css | 72 +- .../pages/TitlesPage/TitlesPage.module.css | 60 +- .../src/pages/TitlesPage/TitlesPage.tsx | 142 ++-- modules/frontend/vite.config.ts | 6 +- 37 files changed, 797 insertions(+), 1247 deletions(-) rename modules/frontend/src/{api_/models/cursor.ts => api/models/CursorObj.ts} (66%) rename modules/frontend/src/{api_/models/Tags.ts => api/models/TitleSort.ts} (60%) rename modules/frontend/src/{api_/models/Title.ts => api/models/title_sort.ts} (61%) delete mode 100644 modules/frontend/src/api_/core/ApiError.ts delete mode 100644 modules/frontend/src/api_/core/ApiRequestOptions.ts delete mode 100644 modules/frontend/src/api_/core/ApiResult.ts delete mode 100644 modules/frontend/src/api_/core/CancelablePromise.ts delete mode 100644 modules/frontend/src/api_/core/OpenAPI.ts delete mode 100644 modules/frontend/src/api_/core/request.ts delete mode 100644 modules/frontend/src/api_/index.ts delete mode 100644 modules/frontend/src/api_/models/Image.ts delete mode 100644 modules/frontend/src/api_/models/ReleaseSeason.ts delete mode 100644 modules/frontend/src/api_/models/Review.ts delete mode 100644 modules/frontend/src/api_/models/Studio.ts delete mode 100644 modules/frontend/src/api_/models/Tag.ts delete mode 100644 modules/frontend/src/api_/models/TitleStatus.ts delete mode 100644 modules/frontend/src/api_/models/User.ts delete mode 100644 modules/frontend/src/api_/models/UserTitle.ts delete mode 100644 modules/frontend/src/api_/models/UserTitleStatus.ts delete mode 100644 modules/frontend/src/api_/services/DefaultService.ts delete mode 100644 modules/frontend/src/components/ListView/useListView.tsx diff --git a/modules/frontend/package-lock.json b/modules/frontend/package-lock.json index 6a06afb..f5dde46 100644 --- a/modules/frontend/package-lock.json +++ b/modules/frontend/package-lock.json @@ -8,10 +8,13 @@ "name": "nyanimedb-frontend", "version": "0.0.0", "dependencies": { + "@heroicons/react": "^2.2.0", + "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "tailwindcss": "^4.1.17" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -337,7 +340,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -354,7 +356,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -371,7 +372,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -388,7 +388,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -405,7 +404,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -422,7 +420,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -439,7 +436,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -456,7 +452,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -473,7 +468,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -490,7 +484,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -507,7 +500,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -524,7 +516,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -541,7 +532,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -558,7 +548,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -575,7 +564,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -592,7 +580,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -609,7 +596,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -626,7 +612,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -643,7 +628,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -660,7 +644,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -677,7 +660,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -694,7 +676,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -711,7 +692,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -728,7 +708,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -745,7 +724,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -762,7 +740,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -929,6 +906,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -985,7 +971,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -996,7 +981,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1007,7 +991,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1017,14 +1000,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1090,7 +1071,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1104,7 +1084,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1118,7 +1097,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1132,7 +1110,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1146,7 +1123,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1160,7 +1136,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1174,7 +1149,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1188,7 +1162,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1202,7 +1175,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1216,7 +1188,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1230,7 +1201,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1244,7 +1214,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1258,7 +1227,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1272,7 +1240,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1286,7 +1253,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1300,7 +1266,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1314,7 +1279,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1328,7 +1292,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1342,7 +1305,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1356,7 +1318,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1370,7 +1331,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1384,13 +1344,269 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1440,7 +1656,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1454,7 +1669,7 @@ "version": "24.7.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -2127,6 +2342,15 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2148,6 +2372,19 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2197,7 +2434,6 @@ "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -2617,7 +2853,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2726,7 +2961,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -2884,6 +3118,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2988,6 +3231,255 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3021,6 +3513,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3109,7 +3610,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -3249,7 +3749,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3269,7 +3768,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3437,7 +3935,6 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3558,7 +4055,6 @@ "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" @@ -3590,11 +4086,29 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -3611,7 +4125,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3629,7 +4142,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -3735,7 +4247,7 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -3793,7 +4305,6 @@ "version": "7.1.9", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -3869,7 +4380,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -3887,7 +4397,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { diff --git a/modules/frontend/package.json b/modules/frontend/package.json index b4977aa..beb2b2a 100644 --- a/modules/frontend/package.json +++ b/modules/frontend/package.json @@ -10,10 +10,13 @@ "preview": "vite preview" }, "dependencies": { + "@heroicons/react": "^2.2.0", + "@tailwindcss/vite": "^4.1.17", "axios": "^1.12.2", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router-dom": "^7.9.4" + "react-router-dom": "^7.9.4", + "tailwindcss": "^4.1.17" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/modules/frontend/src/App.tsx b/modules/frontend/src/App.tsx index 1256086..f67c37e 100644 --- a/modules/frontend/src/App.tsx +++ b/modules/frontend/src/App.tsx @@ -1,7 +1,7 @@ import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import UserPage from "./pages/UserPage/UserPage"; -import TitlesPage from "./pages/UserPage/UserPage"; +import TitlesPage from "./pages/TitlesPage/TitlesPage"; const App: React.FC = () => { return ( diff --git a/modules/frontend/src/api/index.ts b/modules/frontend/src/api/index.ts index f0d09ee..80ae491 100644 --- a/modules/frontend/src/api/index.ts +++ b/modules/frontend/src/api/index.ts @@ -8,16 +8,19 @@ export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; export type { cursor } from './models/cursor'; +export type { CursorObj } from './models/CursorObj'; export type { Image } from './models/Image'; -export { ReleaseSeason } from './models/ReleaseSeason'; +export type { ReleaseSeason } from './models/ReleaseSeason'; export type { Review } from './models/Review'; export type { Studio } from './models/Studio'; export type { Tag } from './models/Tag'; export type { Tags } from './models/Tags'; export type { Title } from './models/Title'; -export { TitleStatus } from './models/TitleStatus'; +export type { title_sort } from './models/title_sort'; +export type { TitleSort } from './models/TitleSort'; +export type { TitleStatus } from './models/TitleStatus'; export type { User } from './models/User'; export type { UserTitle } from './models/UserTitle'; -export { UserTitleStatus } from './models/UserTitleStatus'; +export type { UserTitleStatus } from './models/UserTitleStatus'; export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api_/models/cursor.ts b/modules/frontend/src/api/models/CursorObj.ts similarity index 66% rename from modules/frontend/src/api_/models/cursor.ts rename to modules/frontend/src/api/models/CursorObj.ts index 5788e14..f54abb1 100644 --- a/modules/frontend/src/api_/models/cursor.ts +++ b/modules/frontend/src/api/models/CursorObj.ts @@ -2,4 +2,8 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type cursor = string; +export type CursorObj = { + id: number; + param?: string; +}; + diff --git a/modules/frontend/src/api/models/ReleaseSeason.ts b/modules/frontend/src/api/models/ReleaseSeason.ts index 182b980..ad9f930 100644 --- a/modules/frontend/src/api/models/ReleaseSeason.ts +++ b/modules/frontend/src/api/models/ReleaseSeason.ts @@ -5,9 +5,4 @@ /** * Title release season */ -export enum ReleaseSeason { - WINTER = 'winter', - SPRING = 'spring', - SUMMER = 'summer', - FALL = 'fall', -} +export type ReleaseSeason = 'winter' | 'spring' | 'summer' | 'fall'; diff --git a/modules/frontend/src/api_/models/Tags.ts b/modules/frontend/src/api/models/TitleSort.ts similarity index 60% rename from modules/frontend/src/api_/models/Tags.ts rename to modules/frontend/src/api/models/TitleSort.ts index 748f066..1c9385e 100644 --- a/modules/frontend/src/api_/models/Tags.ts +++ b/modules/frontend/src/api/models/TitleSort.ts @@ -2,8 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Tag } from './Tag'; /** - * Array of localized tags + * Title sort order */ -export type Tags = Array; +export type TitleSort = 'id' | 'year' | 'rating' | 'views'; diff --git a/modules/frontend/src/api/models/TitleStatus.ts b/modules/frontend/src/api/models/TitleStatus.ts index 811ece8..72e0261 100644 --- a/modules/frontend/src/api/models/TitleStatus.ts +++ b/modules/frontend/src/api/models/TitleStatus.ts @@ -5,8 +5,4 @@ /** * Title status */ -export enum TitleStatus { - FINISHED = 'finished', - ONGOING = 'ongoing', - PLANNED = 'planned', -} +export type TitleStatus = 'finished' | 'ongoing' | 'planned'; diff --git a/modules/frontend/src/api/models/User.ts b/modules/frontend/src/api/models/User.ts index 541028e..cd76dbe 100644 --- a/modules/frontend/src/api/models/User.ts +++ b/modules/frontend/src/api/models/User.ts @@ -6,11 +6,11 @@ export type User = { /** * Unique user ID (primary key) */ - id: number; + id?: number; /** * ID of the user avatar (references images table) */ - avatar_id?: number | null; + avatar_id?: number; /** * User email */ diff --git a/modules/frontend/src/api/models/UserTitleStatus.ts b/modules/frontend/src/api/models/UserTitleStatus.ts index 20651fe..0a29626 100644 --- a/modules/frontend/src/api/models/UserTitleStatus.ts +++ b/modules/frontend/src/api/models/UserTitleStatus.ts @@ -5,9 +5,4 @@ /** * User's title status */ -export enum UserTitleStatus { - FINISHED = 'finished', - PLANNED = 'planned', - DROPPED = 'dropped', - IN_PROGRESS = 'in-progress', -} +export type UserTitleStatus = 'finished' | 'planned' | 'dropped' | 'in-progress'; diff --git a/modules/frontend/src/api_/models/Title.ts b/modules/frontend/src/api/models/title_sort.ts similarity index 61% rename from modules/frontend/src/api_/models/Title.ts rename to modules/frontend/src/api/models/title_sort.ts index 4da7aa3..69b01a7 100644 --- a/modules/frontend/src/api_/models/Title.ts +++ b/modules/frontend/src/api/models/title_sort.ts @@ -2,4 +2,5 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type Title = Record; +import type { TitleSort } from './TitleSort'; +export type title_sort = TitleSort; diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index b0ae54d..52321b8 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -2,17 +2,23 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { CursorObj } from '../models/CursorObj'; import type { ReleaseSeason } from '../models/ReleaseSeason'; import type { Title } from '../models/Title'; +import type { TitleSort } from '../models/TitleSort'; import type { TitleStatus } from '../models/TitleStatus'; import type { User } from '../models/User'; import type { UserTitle } from '../models/UserTitle'; +import type { UserTitleStatus } from '../models/UserTitleStatus'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class DefaultService { /** * Get titles + * @param cursor + * @param sort + * @param sortForward * @param word * @param status * @param rating @@ -21,10 +27,13 @@ export class DefaultService { * @param limit * @param offset * @param fields - * @returns Title List of titles + * @returns any List of titles with cursor * @throws ApiError */ public static getTitles( + cursor?: string, + sort?: TitleSort, + sortForward: boolean = true, word?: string, status?: TitleStatus, rating?: number, @@ -33,11 +42,20 @@ export class DefaultService { limit: number = 10, offset?: number, fields: string = 'all', - ): CancelablePromise> { + ): CancelablePromise<{ + /** + * List of titles + */ + data: Array; + cursor: CursorObj; + }> { return __request(OpenAPI, { method: 'GET', url: '/titles', query: { + 'cursor': cursor, + 'sort': sort, + 'sort_forward': sortForward, 'word': word, 'status': status, 'rating': rating, @@ -111,9 +129,13 @@ export class DefaultService { * Get user titles * @param userId * @param cursor - * @param query + * @param word + * @param status + * @param watchStatus + * @param rating + * @param releaseYear + * @param releaseSeason * @param limit - * @param offset * @param fields * @returns UserTitle List of user titles * @throws ApiError @@ -121,22 +143,30 @@ export class DefaultService { public static getUsersTitles( userId: string, cursor?: string, - query?: string, + word?: string, + status?: TitleStatus, + watchStatus?: UserTitleStatus, + rating?: number, + releaseYear?: number, + releaseSeason?: ReleaseSeason, limit: number = 10, - offset?: number, fields: string = 'all', ): CancelablePromise<Array<UserTitle>> { return __request(OpenAPI, { method: 'GET', - url: '/users/{user_id}/titles', + url: '/users/{user_id}/titles/', path: { 'user_id': userId, }, query: { 'cursor': cursor, - 'query': query, + 'word': word, + 'status': status, + 'watch_status': watchStatus, + 'rating': rating, + 'release_year': releaseYear, + 'release_season': releaseSeason, 'limit': limit, - 'offset': offset, 'fields': fields, }, errors: { diff --git a/modules/frontend/src/api_/core/ApiError.ts b/modules/frontend/src/api_/core/ApiError.ts deleted file mode 100644 index ec7b16a..0000000 --- a/modules/frontend/src/api_/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: any; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} diff --git a/modules/frontend/src/api_/core/ApiRequestOptions.ts b/modules/frontend/src/api_/core/ApiRequestOptions.ts deleted file mode 100644 index 93143c3..0000000 --- a/modules/frontend/src/api_/core/ApiRequestOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiRequestOptions = { - readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - readonly url: string; - readonly path?: Record<string, any>; - readonly cookies?: Record<string, any>; - readonly headers?: Record<string, any>; - readonly query?: Record<string, any>; - readonly formData?: Record<string, any>; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly errors?: Record<number, string>; -}; diff --git a/modules/frontend/src/api_/core/ApiResult.ts b/modules/frontend/src/api_/core/ApiResult.ts deleted file mode 100644 index ee1126e..0000000 --- a/modules/frontend/src/api_/core/ApiResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiResult = { - readonly url: string; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly body: any; -}; diff --git a/modules/frontend/src/api_/core/CancelablePromise.ts b/modules/frontend/src/api_/core/CancelablePromise.ts deleted file mode 100644 index d70de92..0000000 --- a/modules/frontend/src/api_/core/CancelablePromise.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export class CancelError extends Error { - - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise<T> implements Promise<T> { - #isResolved: boolean; - #isRejected: boolean; - #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; - readonly #promise: Promise<T>; - #resolve?: (value: T | PromiseLike<T>) => void; - #reject?: (reason?: any) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike<T>) => void, - reject: (reason?: any) => void, - onCancel: OnCancel - ) => void - ) { - this.#isResolved = false; - this.#isRejected = false; - this.#isCancelled = false; - this.#cancelHandlers = []; - this.#promise = new Promise<T>((resolve, reject) => { - this.#resolve = resolve; - this.#reject = reject; - - const onResolve = (value: T | PromiseLike<T>): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isResolved = true; - if (this.#resolve) this.#resolve(value); - }; - - const onReject = (reason?: any): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isRejected = true; - if (this.#reject) this.#reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this.#isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this.#isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this.#isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then<TResult1 = T, TResult2 = never>( - onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null, - onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null - ): Promise<TResult1 | TResult2> { - return this.#promise.then(onFulfilled, onRejected); - } - - public catch<TResult = never>( - onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null - ): Promise<T | TResult> { - return this.#promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise<T> { - return this.#promise.finally(onFinally); - } - - public cancel(): void { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isCancelled = true; - if (this.#cancelHandlers.length) { - try { - for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this.#isCancelled; - } -} diff --git a/modules/frontend/src/api_/core/OpenAPI.ts b/modules/frontend/src/api_/core/OpenAPI.ts deleted file mode 100644 index 185e5c3..0000000 --- a/modules/frontend/src/api_/core/OpenAPI.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Resolver<T> = (options: ApiRequestOptions) => Promise<T>; -type Headers = Record<string, string>; - -export type OpenAPIConfig = { - BASE: string; - VERSION: string; - WITH_CREDENTIALS: boolean; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - TOKEN?: string | Resolver<string> | undefined; - USERNAME?: string | Resolver<string> | undefined; - PASSWORD?: string | Resolver<string> | undefined; - HEADERS?: Headers | Resolver<Headers> | undefined; - ENCODE_PATH?: ((path: string) => string) | undefined; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '/api/v1', - VERSION: '1.0.0', - WITH_CREDENTIALS: false, - CREDENTIALS: 'include', - TOKEN: undefined, - USERNAME: undefined, - PASSWORD: undefined, - HEADERS: undefined, - ENCODE_PATH: undefined, -}; diff --git a/modules/frontend/src/api_/core/request.ts b/modules/frontend/src/api_/core/request.ts deleted file mode 100644 index 1dc6fef..0000000 --- a/modules/frontend/src/api_/core/request.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import axios from 'axios'; -import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; -import FormData from 'form-data'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => { - return value !== undefined && value !== null; -}; - -export const isString = (value: any): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: any): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return ( - typeof value === 'object' && - typeof value.type === 'string' && - typeof value.stream === 'function' && - typeof value.arrayBuffer === 'function' && - typeof value.constructor === 'function' && - typeof value.constructor.name === 'string' && - /^(Blob|File)$/.test(value.constructor.name) && - /^(Blob|File)$/.test(value[Symbol.toStringTag]) - ); -}; - -export const isFormData = (value: any): value is FormData => { - return value instanceof FormData; -}; - -export const isSuccess = (status: number): boolean => { - return status >= 200 && status < 300; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record<string, any>): string => { - const qs: string[] = []; - - const append = (key: string, value: any) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const process = (key: string, value: any) => { - if (isDefined(value)) { - if (Array.isArray(value)) { - value.forEach(v => { - process(key, v); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => { - process(`${key}[${k}]`, v); - }); - } else { - append(key, value); - } - } - }; - - Object.entries(params).forEach(([key, value]) => { - process(key, value); - }); - - if (qs.length > 0) { - return `?${qs.join('&')}`; - } - - return ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = `${config.BASE}${path}`; - if (options.query) { - return `${url}${getQueryString(options.query)}`; - } - return url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: any) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([_, value]) => isDefined(value)) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver<T> = (options: ApiRequestOptions) => Promise<T>; - -export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => { - if (typeof resolver === 'function') { - return (resolver as Resolver<T>)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - ...formHeaders, - }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record<string, string>); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } - - return headers; -}; - -export const getRequestBody = (options: ApiRequestOptions): any => { - if (options.body) { - return options.body; - } - return undefined; -}; - -export const sendRequest = async <T>( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Record<string, string>, - onCancel: OnCancel, - axiosClient: AxiosInstance -): Promise<AxiosResponse<T>> => { - const source = axios.CancelToken.source(); - - const requestConfig: AxiosRequestConfig = { - url, - headers, - data: body ?? formData, - method: options.method, - withCredentials: config.WITH_CREDENTIALS, - withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, - cancelToken: source.token, - }; - - onCancel(() => source.cancel('The user aborted a request.')); - - try { - return await axiosClient.request(requestConfig); - } catch (error) { - const axiosError = error as AxiosError<T>; - if (axiosError.response) { - return axiosError.response; - } - throw error; - } -}; - -export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers[responseHeader]; - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = (response: AxiosResponse<any>): any => { - if (response.status !== 204) { - return response.data; - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record<number, string> = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @param axiosClient The axios client instance to use - * @returns CancelablePromise<T> - * @throws ApiError - */ -export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options, formData); - - if (!onCancel.isCancelled) { - const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient); - const responseBody = getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: isSuccess(response.status), - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/modules/frontend/src/api_/index.ts b/modules/frontend/src/api_/index.ts deleted file mode 100644 index f0d09ee..0000000 --- a/modules/frontend/src/api_/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; - -export type { cursor } from './models/cursor'; -export type { Image } from './models/Image'; -export { ReleaseSeason } from './models/ReleaseSeason'; -export type { Review } from './models/Review'; -export type { Studio } from './models/Studio'; -export type { Tag } from './models/Tag'; -export type { Tags } from './models/Tags'; -export type { Title } from './models/Title'; -export { TitleStatus } from './models/TitleStatus'; -export type { User } from './models/User'; -export type { UserTitle } from './models/UserTitle'; -export { UserTitleStatus } from './models/UserTitleStatus'; - -export { DefaultService } from './services/DefaultService'; diff --git a/modules/frontend/src/api_/models/Image.ts b/modules/frontend/src/api_/models/Image.ts deleted file mode 100644 index 1317db7..0000000 --- a/modules/frontend/src/api_/models/Image.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type Image = { - id?: number; - storage_type?: string; - image_path?: string; -}; - diff --git a/modules/frontend/src/api_/models/ReleaseSeason.ts b/modules/frontend/src/api_/models/ReleaseSeason.ts deleted file mode 100644 index 182b980..0000000 --- a/modules/frontend/src/api_/models/ReleaseSeason.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * Title release season - */ -export enum ReleaseSeason { - WINTER = 'winter', - SPRING = 'spring', - SUMMER = 'summer', - FALL = 'fall', -} diff --git a/modules/frontend/src/api_/models/Review.ts b/modules/frontend/src/api_/models/Review.ts deleted file mode 100644 index 9b453b7..0000000 --- a/modules/frontend/src/api_/models/Review.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type Review = Record<string, any>; diff --git a/modules/frontend/src/api_/models/Studio.ts b/modules/frontend/src/api_/models/Studio.ts deleted file mode 100644 index 062695a..0000000 --- a/modules/frontend/src/api_/models/Studio.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { Image } from './Image'; -export type Studio = { - id: number; - name: string; - poster?: Image; - description?: string; -}; - diff --git a/modules/frontend/src/api_/models/Tag.ts b/modules/frontend/src/api_/models/Tag.ts deleted file mode 100644 index 665c724..0000000 --- a/modules/frontend/src/api_/models/Tag.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * A localized tag: keys are language codes (ISO 639-1), values are tag names - */ -export type Tag = Record<string, string>; diff --git a/modules/frontend/src/api_/models/TitleStatus.ts b/modules/frontend/src/api_/models/TitleStatus.ts deleted file mode 100644 index 811ece8..0000000 --- a/modules/frontend/src/api_/models/TitleStatus.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * Title status - */ -export enum TitleStatus { - FINISHED = 'finished', - ONGOING = 'ongoing', - PLANNED = 'planned', -} diff --git a/modules/frontend/src/api_/models/User.ts b/modules/frontend/src/api_/models/User.ts deleted file mode 100644 index 541028e..0000000 --- a/modules/frontend/src/api_/models/User.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type User = { - /** - * Unique user ID (primary key) - */ - id: number; - /** - * ID of the user avatar (references images table) - */ - avatar_id?: number | null; - /** - * User email - */ - mail?: string; - /** - * Username (alphanumeric + _ or -) - */ - nickname: string; - /** - * Display name - */ - disp_name?: string; - /** - * User description - */ - user_desc?: string; - /** - * Timestamp when the user was created - */ - creation_date?: string; -}; - diff --git a/modules/frontend/src/api_/models/UserTitle.ts b/modules/frontend/src/api_/models/UserTitle.ts deleted file mode 100644 index 26d5ddc..0000000 --- a/modules/frontend/src/api_/models/UserTitle.ts +++ /dev/null @@ -1,5 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserTitle = Record<string, any>; diff --git a/modules/frontend/src/api_/models/UserTitleStatus.ts b/modules/frontend/src/api_/models/UserTitleStatus.ts deleted file mode 100644 index 20651fe..0000000 --- a/modules/frontend/src/api_/models/UserTitleStatus.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * User's title status - */ -export enum UserTitleStatus { - FINISHED = 'finished', - PLANNED = 'planned', - DROPPED = 'dropped', - IN_PROGRESS = 'in-progress', -} diff --git a/modules/frontend/src/api_/services/DefaultService.ts b/modules/frontend/src/api_/services/DefaultService.ts deleted file mode 100644 index b0ae54d..0000000 --- a/modules/frontend/src/api_/services/DefaultService.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ReleaseSeason } from '../models/ReleaseSeason'; -import type { Title } from '../models/Title'; -import type { TitleStatus } from '../models/TitleStatus'; -import type { User } from '../models/User'; -import type { UserTitle } from '../models/UserTitle'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class DefaultService { - /** - * Get titles - * @param word - * @param status - * @param rating - * @param releaseYear - * @param releaseSeason - * @param limit - * @param offset - * @param fields - * @returns Title List of titles - * @throws ApiError - */ - public static getTitles( - word?: string, - status?: TitleStatus, - rating?: number, - releaseYear?: number, - releaseSeason?: ReleaseSeason, - limit: number = 10, - offset?: number, - fields: string = 'all', - ): CancelablePromise<Array<Title>> { - return __request(OpenAPI, { - method: 'GET', - url: '/titles', - query: { - 'word': word, - 'status': status, - 'rating': rating, - 'release_year': releaseYear, - 'release_season': releaseSeason, - 'limit': limit, - 'offset': offset, - 'fields': fields, - }, - errors: { - 400: `Request params are not correct`, - 500: `Unknown server error`, - }, - }); - } - /** - * Get title description - * @param titleId - * @param fields - * @returns Title Title description - * @throws ApiError - */ - public static getTitles1( - titleId: number, - fields: string = 'all', - ): CancelablePromise<Title> { - return __request(OpenAPI, { - method: 'GET', - url: '/titles/{title_id}', - path: { - 'title_id': titleId, - }, - query: { - 'fields': fields, - }, - errors: { - 400: `Request params are not correct`, - 404: `Title not found`, - 500: `Unknown server error`, - }, - }); - } - /** - * Get user info - * @param userId - * @param fields - * @returns User User info - * @throws ApiError - */ - public static getUsers( - userId: string, - fields: string = 'all', - ): CancelablePromise<User> { - return __request(OpenAPI, { - method: 'GET', - url: '/users/{user_id}', - path: { - 'user_id': userId, - }, - query: { - 'fields': fields, - }, - errors: { - 400: `Request params are not correct`, - 404: `User not found`, - 500: `Unknown server error`, - }, - }); - } - /** - * Get user titles - * @param userId - * @param cursor - * @param query - * @param limit - * @param offset - * @param fields - * @returns UserTitle List of user titles - * @throws ApiError - */ - public static getUsersTitles( - userId: string, - cursor?: string, - query?: string, - limit: number = 10, - offset?: number, - fields: string = 'all', - ): CancelablePromise<Array<UserTitle>> { - return __request(OpenAPI, { - method: 'GET', - url: '/users/{user_id}/titles', - path: { - 'user_id': userId, - }, - query: { - 'cursor': cursor, - 'query': query, - 'limit': limit, - 'offset': offset, - 'fields': fields, - }, - errors: { - 400: `Request params are not correct`, - 500: `Unknown server error`, - }, - }); - } -} diff --git a/modules/frontend/src/components/ListView/ListView.tsx b/modules/frontend/src/components/ListView/ListView.tsx index 77fea97..e0e8ab9 100644 --- a/modules/frontend/src/components/ListView/ListView.tsx +++ b/modules/frontend/src/components/ListView/ListView.tsx @@ -1,52 +1,103 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid"; +import type { CursorObj } from "../../api"; -interface ListViewProps<TItem> { - hook: ReturnType<typeof import("./useListView.tsx").useListView<TItem>>; - renderHorizontal: (item: TItem) => React.ReactNode; - renderSquare: (item: TItem) => React.ReactNode; -} +export type ListViewProps<T> = { + fetchItems: (cursor: string, limit: number) => Promise<{ items: T[]; cursor: CursorObj}>; + renderItem: (item: T, layout: "square" | "horizontal") => React.ReactNode; + pageSize?: number; + searchPlaceholder?: string; + setSearch: any; +}; -export function ListView<TItem>({ - hook, - renderHorizontal, - renderSquare -}: ListViewProps<TItem>) { - const { items, search, setSearch, viewMode, setViewMode, loadMore, hasMore } = hook; +export function ListView<T>({ + fetchItems, + renderItem, + pageSize = 20, + searchPlaceholder = "Search...", +}: ListViewProps<T>) { + const [items, setItems] = useState<T[]>([]); + const [cursorObj, setCursorObj] = useState<CursorObj | undefined>(undefined); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [search, setSearch] = useState(""); + const [layout, setLayout] = useState<"square" | "horizontal">("horizontal"); + const [error, setError] = useState<string | null>(null); + + const loadItems = async (reset: boolean = false) => { + try { + if (reset) { + setLoading(true); + setCursorObj(undefined); + } else { + setLoadingMore(true); + } + + const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)) : "" + console.log("encoded cursor: " + cursorStr) + + const result = await fetchItems(cursorStr, pageSize); + + if (reset) setItems(result.items); + else setItems(prev => [...prev, ...result.items]); + + setCursorObj(result.cursor); + setError(null); + } catch (err: any) { + console.error(err); + setError("Failed to fetch items."); + } finally { + setLoading(false); + setLoadingMore(false); + } + }; + + useEffect(() => { + loadItems(true); + }, [search]); return ( - <div> - {/* Search + Layout Switcher */} - <div style={{ display: "flex", gap: 8, marginBottom: 16 }}> + <div className="w-full min-h-screen bg-gray-50 p-6 text-black flex flex-col items-center"> + <div className="w-full sm:w-4/5 flex gap-4 mb-8"> <input - placeholder="Search..." - value={search} + type="text" + placeholder={searchPlaceholder} + // value={search} onChange={e => setSearch(e.target.value)} + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-black" /> - - <button onClick={() => setViewMode("horizontal")}>Horizontal</button> - <button onClick={() => setViewMode("square")}>Square</button> - </div> - - {/* Items */} - <div - style={{ - display: "grid", - gridTemplateColumns: viewMode === "square" ? "repeat(auto-fill, 160px)" : "1fr", - gap: 12 - }} - > - {items.map(item => - viewMode === "horizontal" - ? renderHorizontal(item) - : renderSquare(item) - )} - </div> - - {hasMore && ( - <button onClick={loadMore} style={{ marginTop: 16 }}> - Load More + <button + className="p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition" + onClick={() => + setLayout(prev => (prev === "square" ? "horizontal" : "square")) + }> + {layout === "square" ? <Squares2X2Icon className="w-6 h-6" /> : <Bars3Icon className="w-6 h-6" />} </button> + </div> + + {error && <div className="text-red-600 mb-6 font-medium">{error}</div>} + + <div + className={`w-full sm:w-4/5 grid gap-6 ${ + layout === "square" ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" : "grid-cols-1" + }`} + > + {items.map(item => renderItem(item, layout))} + </div> + + {cursorObj && ( + <div className="mt-8 flex justify-center w-full sm:w-4/5"> + <button + className="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed" + onClick={() => loadItems(false)} + disabled={loadingMore} + > + {loadingMore ? "Loading..." : "Load More"} + </button> + </div> )} + + {loading && <div className="mt-20 font-medium">Loading...</div>} </div> ); -} \ No newline at end of file +} diff --git a/modules/frontend/src/components/ListView/useListView.tsx b/modules/frontend/src/components/ListView/useListView.tsx deleted file mode 100644 index 20c3597..0000000 --- a/modules/frontend/src/components/ListView/useListView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useState, useEffect } from "react"; -import type { FetchFunction } from "../../types/list"; - -export function useListView<TItem>(fetchFn: FetchFunction<TItem>) { - const [items, setItems] = useState<TItem[]>([]); - const [cursor, setCursor] = useState<string | undefined>(); - const [search, setSearch] = useState(""); - const [viewMode, setViewMode] = useState<"horizontal" | "square">("horizontal"); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - loadItems(true); - }, [search]); - - const loadItems = async (reset = false) => { - setIsLoading(true); - const result = await fetchFn({ - search, - cursor: reset ? undefined : cursor, - }); - - setItems(prev => reset ? result.items : [...prev, ...result.items]); - setCursor(result.nextCursor); - setIsLoading(false); - }; - - return { - items, - search, - setSearch, - viewMode, - setViewMode, - loadMore: () => loadItems(), - hasMore: Boolean(cursor), - isLoading, - }; -} \ No newline at end of file diff --git a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx index c3a8159..cde6037 100644 --- a/modules/frontend/src/components/cards/TitleCardHorizontal.tsx +++ b/modules/frontend/src/components/cards/TitleCardHorizontal.tsx @@ -9,13 +9,13 @@ export function TitleCardHorizontal({ title }: { title: Title }) { border: "1px solid #ddd", borderRadius: 8 }}> - {title.posterUrl && ( - <img src={title.posterUrl} width={80} /> + {title.poster?.image_path && ( + <img src={title.poster.image_path} width={80} /> )} <div> - <h3>{title.name}</h3> - <p>{title.year} · {title.season} · Rating: {title.rating}</p> - <p>Status: {title.status}</p> + <h3>{title.title_names["en"]}</h3> + <p>{title.release_year} · {title.release_season} · Rating: {title.rating}</p> + <p>Status: {title.title_status}</p> </div> </div> ); diff --git a/modules/frontend/src/components/cards/TitleCardSquare.tsx b/modules/frontend/src/components/cards/TitleCardSquare.tsx index 0fc0339..e21c258 100644 --- a/modules/frontend/src/components/cards/TitleCardSquare.tsx +++ b/modules/frontend/src/components/cards/TitleCardSquare.tsx @@ -10,12 +10,12 @@ export function TitleCardSquare({ title }: { title: Title }) { borderRadius: 8, textAlign: "center" }}> - {title.posterUrl && ( - <img src={title.posterUrl} width={140} /> + {title.poster?.image_path && ( + <img src={title.poster.image_path} width={140} /> )} <div> - <h4>{title.name}</h4> - <small>{title.year} • {title.rating}</small> + <h4>{title.title_names["en"]}</h4> + <small>{title.release_year} • {title.rating}</small> </div> </div> ); diff --git a/modules/frontend/src/index.css b/modules/frontend/src/index.css index 08a3ac9..e20de02 100644 --- a/modules/frontend/src/index.css +++ b/modules/frontend/src/index.css @@ -1,68 +1,8 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; +@import "tailwindcss"; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { +html, body, #root { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} + padding: 0; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css index 9cc728b..f1d8c73 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.module.css @@ -1,59 +1 @@ -.container { - padding: 24px; -} - -.header { - display: flex; - justify-content: space-between; - margin-bottom: 16px; -} - -.searchInput { - padding: 8px; - width: 240px; -} - -.list { - display: grid; - gap: 12px; -} - -.card { - display: flex; - padding: 10px; - border: 1px solid #ddd; - border-radius: 8px; - gap: 12px; -} - -.poster { - width: 80px; - height: 120px; - object-fit: cover; - border-radius: 4px; -} - -.posterPlaceholder { - width: 80px; - height: 120px; - background: #eee; - display: flex; - align-items: center; - justify-content: center; -} - -.cardInfo { - display: flex; - flex-direction: column; -} - -.loadMore { - margin-top: 16px; - padding: 8px 16px; -} - -.loader, -.error { - padding: 20px; - text-align: center; -} +@import "tailwindcss"; diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index 438d828..b59f737 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -1,114 +1,52 @@ -import React, { useEffect, useState } from "react"; +import { ListView } from "../../components/ListView/ListView"; import { DefaultService } from "../../api/services/DefaultService"; -import type { Title } from "../../api/models/Title"; -import styles from "./TitlesPage.module.css"; +import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; +import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; +import type { Title } from "../../api"; +import { useState, useEffect } from "react"; -const LIMIT = 20; - -const TitlesPage: React.FC = () => { - const [titles, setTitles] = useState<Title[]>([]); +const PAGE_SIZE = 20; +export default function TitlesPage() { const [search, setSearch] = useState(""); - const [offset, setOffset] = useState(0); - const [loading, setLoading] = useState(true); - const [loadingMore, setLoadingMore] = useState(false); - const [error, setError] = useState<string | null>(null); + const loadTitles = async (cursor: string, limit: number) => { + const result = await DefaultService.getTitles( + cursor, + undefined, + true, + search, + undefined, + undefined, + undefined, + undefined, + limit, + undefined, + 'all' + ); - const fetchTitles = async (reset: boolean) => { - try { - if (reset) { - setLoading(true); - setOffset(0); - } else { - setLoadingMore(true); - } - - const result = await DefaultService.getTitles( - search || undefined, - undefined, // status - undefined, // rating - undefined, // release_year - undefined, // release_season - LIMIT, - reset ? 0 : offset, - "all" - ); - - if (reset) { - setTitles(result); - } else { - setTitles(prev => [...prev, ...result]); - } - - if (result.length > 0) { - setOffset(prev => prev + LIMIT); - } - - } catch (err) { - console.error(err); - setError("Failed to fetch titles."); - } finally { - setLoading(false); - setLoadingMore(false); - } + return { + items: result.data ?? [], + cursor: result.cursor ?? null, + }; }; - useEffect(() => { - fetchTitles(true); - }, [search]); - - if (loading) return <div className={styles.loader}>Loading...</div>; - if (error) return <div className={styles.error}>{error}</div>; - return ( - <div className={styles.container}> - <div className={styles.header}> - <h1>Titles</h1> + <div className="w-full min-h-screen bg-gray-50 p-6 text-black flex flex-col items-center"> + + <h1 className="text-4xl font-bold mb-6 text-center">Titles</h1> - <input - className={styles.searchInput} - placeholder="Search titles..." - value={search} - onChange={e => setSearch(e.target.value)} - /> - </div> - - <div className={styles.list}> - {titles.map((t) => ( - <div key={t.id} className={styles.card}> - {t.poster_id ? ( - <img - src={`/images/${t.poster_id}.png`} - alt="Poster" - className={styles.poster} - /> - ) : ( - <div className={styles.posterPlaceholder}>No Image</div> - )} - - <div className={styles.cardInfo}> - <h3 className={styles.titleName}>{t.name}</h3> - <p className={styles.meta}> - {t.release_year} • {t.release_season} - </p> - <p className={styles.rating}>Rating: {t.rating}</p> - <p className={styles.status}>{t.status}</p> - </div> - </div> - ))} - </div> - - {titles.length > 0 && ( - <button - className={styles.loadMore} - onClick={() => fetchTitles(false)} - disabled={loadingMore} - > - {loadingMore ? "Loading..." : "Load More"} - </button> - )} + <ListView<Title> + pageSize={PAGE_SIZE} + fetchItems={loadTitles} + searchPlaceholder="Search titles..." + renderItem={(title, layout) => + layout === "square" + ? <TitleCardSquare title={title} /> + : <TitleCardHorizontal title={title} /> + } + setSearch={setSearch} + /> </div> ); -}; +} -export default TitlesPage; diff --git a/modules/frontend/vite.config.ts b/modules/frontend/vite.config.ts index 4cfbdd0..6c261e6 100644 --- a/modules/frontend/vite.config.ts +++ b/modules/frontend/vite.config.ts @@ -1,9 +1,13 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + tailwindcss() + ], server: { host: '127.0.0.1', port: 8083, From 31a95fabeaa860b02d3be9b565238232235156c1 Mon Sep 17 00:00:00 2001 From: nihonium <nihonium@nekoea.red> Date: Wed, 19 Nov 2025 11:06:09 +0300 Subject: [PATCH 2/2] fix: useEffect --- modules/frontend/src/pages/TitlesPage/TitlesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx index b59f737..4aeb5ec 100644 --- a/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx +++ b/modules/frontend/src/pages/TitlesPage/TitlesPage.tsx @@ -3,7 +3,7 @@ import { DefaultService } from "../../api/services/DefaultService"; import { TitleCardSquare } from "../../components/cards/TitleCardSquare"; import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"; import type { Title } from "../../api"; -import { useState, useEffect } from "react"; +import { useState } from "react"; const PAGE_SIZE = 20; export default function TitlesPage() {