Compare commits
No commits in common. "f1f7feffaa6ab39a3c6579501cc4950645c6201f" and "fd8ecbeacaca78ed52c9da7b6907d10665dbfc75" have entirely different histories.
f1f7feffaa
...
fd8ecbeaca
12 changed files with 154 additions and 624 deletions
240
modules/frontend/package-lock.json
generated
240
modules/frontend/package-lock.json
generated
|
|
@ -8,7 +8,6 @@
|
||||||
"name": "nyanimedb-frontend",
|
"name": "nyanimedb-frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
|
@ -31,9 +30,6 @@
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.45.0",
|
"typescript-eslint": "^8.45.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "20.x"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
|
@ -910,79 +906,6 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
|
||||||
"version": "1.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
|
|
||||||
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/utils": "^0.2.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/dom": {
|
|
||||||
"version": "1.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
|
||||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.7.3",
|
|
||||||
"@floating-ui/utils": "^0.2.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/react": {
|
|
||||||
"version": "0.26.28",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz",
|
|
||||||
"integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/react-dom": "^2.1.2",
|
|
||||||
"@floating-ui/utils": "^0.2.8",
|
|
||||||
"tabbable": "^6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/react-dom": {
|
|
||||||
"version": "2.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
|
|
||||||
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/dom": "^1.7.4"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/utils": {
|
|
||||||
"version": "0.2.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
|
||||||
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@headlessui/react": {
|
|
||||||
"version": "2.2.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz",
|
|
||||||
"integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/react": "^0.26.16",
|
|
||||||
"@react-aria/focus": "^3.20.2",
|
|
||||||
"@react-aria/interactions": "^3.25.0",
|
|
||||||
"@tanstack/react-virtual": "^3.13.9",
|
|
||||||
"use-sync-external-store": "^1.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@heroicons/react": {
|
"node_modules/@heroicons/react": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz",
|
||||||
|
|
@ -1134,103 +1057,6 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-aria/focus": {
|
|
||||||
"version": "3.21.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
|
|
||||||
"integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/interactions": "^3.25.6",
|
|
||||||
"@react-aria/utils": "^3.31.0",
|
|
||||||
"@react-types/shared": "^3.32.1",
|
|
||||||
"@swc/helpers": "^0.5.0",
|
|
||||||
"clsx": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/interactions": {
|
|
||||||
"version": "3.25.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz",
|
|
||||||
"integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/ssr": "^3.9.10",
|
|
||||||
"@react-aria/utils": "^3.31.0",
|
|
||||||
"@react-stately/flags": "^3.1.2",
|
|
||||||
"@react-types/shared": "^3.32.1",
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/ssr": {
|
|
||||||
"version": "3.9.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
|
||||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-aria/utils": {
|
|
||||||
"version": "3.31.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz",
|
|
||||||
"integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@react-aria/ssr": "^3.9.10",
|
|
||||||
"@react-stately/flags": "^3.1.2",
|
|
||||||
"@react-stately/utils": "^3.10.8",
|
|
||||||
"@react-types/shared": "^3.32.1",
|
|
||||||
"@swc/helpers": "^0.5.0",
|
|
||||||
"clsx": "^2.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/flags": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-stately/utils": {
|
|
||||||
"version": "3.10.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
|
|
||||||
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@swc/helpers": "^0.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/shared": {
|
|
||||||
"version": "3.32.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz",
|
|
||||||
"integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.38",
|
"version": "1.0.0-beta.38",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
|
||||||
|
|
@ -1524,15 +1350,6 @@
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
|
||||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
|
||||||
|
|
@ -1790,33 +1607,6 @@
|
||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tanstack/react-virtual": {
|
|
||||||
"version": "3.13.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
|
||||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@tanstack/virtual-core": "3.13.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tanstack/virtual-core": {
|
|
||||||
"version": "3.13.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
|
||||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/tannerlinsley"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -2431,15 +2221,6 @@
|
||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/clsx": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -4305,12 +4086,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tabbable": {
|
|
||||||
"version": "6.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
|
|
||||||
"integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.17",
|
"version": "4.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
|
||||||
|
|
@ -4402,12 +4177,6 @@
|
||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.8.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
|
||||||
"license": "0BSD"
|
|
||||||
},
|
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|
@ -4532,15 +4301,6 @@
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.9",
|
"version": "7.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,10 @@ import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import UserPage from "./pages/UserPage/UserPage";
|
import UserPage from "./pages/UserPage/UserPage";
|
||||||
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
||||||
import { Header } from "./components/Header/Header";
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const username = "nihonium";
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<Header username={username} />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/users/:id" element={<UserPage />} />
|
<Route path="/users/:id" element={<UserPage />} />
|
||||||
<Route path="/titles" element={<TitlesPage />} />
|
<Route path="/titles" element={<TitlesPage />} />
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
type HeaderProps = {
|
|
||||||
username?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ username }) => {
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const toggleMenu = () => setMenuOpen(!menuOpen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="w-full bg-white shadow-md fixed top-0 left-0 z-50">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="flex justify-between h-16 items-center">
|
|
||||||
|
|
||||||
{/* Левый блок — логотип / название */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Link to="/" className="text-xl font-bold text-gray-800 hover:text-blue-600">
|
|
||||||
NyanimeDB
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Центр — ссылки на разделы (desktop) */}
|
|
||||||
<nav className="hidden md:flex space-x-4">
|
|
||||||
<Link to="/titles" className="text-gray-700 hover:text-blue-600">
|
|
||||||
Titles
|
|
||||||
</Link>
|
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600">
|
|
||||||
Users
|
|
||||||
</Link>
|
|
||||||
<Link to="/about" className="text-gray-700 hover:text-blue-600">
|
|
||||||
About
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Правый блок — профиль */}
|
|
||||||
<div className="hidden md:flex items-center space-x-4">
|
|
||||||
{username ? (
|
|
||||||
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium">
|
|
||||||
{username}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium">
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Бургер для мобильного */}
|
|
||||||
<div className="md:hidden flex items-center">
|
|
||||||
<button
|
|
||||||
onClick={toggleMenu}
|
|
||||||
className="p-2 rounded-md hover:bg-gray-200 transition"
|
|
||||||
>
|
|
||||||
{menuOpen ? (
|
|
||||||
<XMarkIcon className="w-6 h-6 text-gray-800" />
|
|
||||||
) : (
|
|
||||||
<Bars3Icon className="w-6 h-6 text-gray-800" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Мобильное меню */}
|
|
||||||
{menuOpen && (
|
|
||||||
<div className="md:hidden bg-white border-t border-gray-200 shadow-md">
|
|
||||||
<nav className="flex flex-col p-4 space-y-2">
|
|
||||||
<Link to="/titles" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Titles</Link>
|
|
||||||
<Link to="/users" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>Users</Link>
|
|
||||||
<Link to="/about" className="text-gray-700 hover:text-blue-600" onClick={() => setMenuOpen(false)}>About</Link>
|
|
||||||
{username ? (
|
|
||||||
<Link to="/profile" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
|
||||||
{username}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link to="/login" className="text-gray-700 hover:text-blue-600 font-medium" onClick={() => setMenuOpen(false)}>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
export type LayoutSwitchProps = {
|
|
||||||
layout: "square" | "horizontal"
|
|
||||||
setLayout: (value: React.SetStateAction<"square" | "horizontal">) => void
|
|
||||||
};
|
|
||||||
|
|
||||||
export function LayoutSwitch({
|
|
||||||
layout,
|
|
||||||
setLayout
|
|
||||||
}: LayoutSwitchProps) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +1,103 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid";
|
import { Squares2X2Icon, Bars3Icon } from "@heroicons/react/24/solid";
|
||||||
|
import type { CursorObj } from "../../api";
|
||||||
|
|
||||||
export type ListViewProps<T> = {
|
export type ListViewProps<T> = {
|
||||||
items: T[];
|
fetchItems: (cursor: string, limit: number) => Promise<{ items: T[]; cursor: CursorObj}>;
|
||||||
layout: "square" | "horizontal";
|
|
||||||
renderItem: (item: T, layout: "square" | "horizontal") => React.ReactNode;
|
renderItem: (item: T, layout: "square" | "horizontal") => React.ReactNode;
|
||||||
onLoadMore: () => void;
|
pageSize?: number;
|
||||||
hasMore: boolean;
|
searchPlaceholder?: string;
|
||||||
loadingMore: boolean;
|
setSearch: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListView<T>({
|
export function ListView<T>({
|
||||||
items,
|
fetchItems,
|
||||||
layout,
|
|
||||||
renderItem,
|
renderItem,
|
||||||
onLoadMore,
|
pageSize = 20,
|
||||||
hasMore,
|
searchPlaceholder = "Search...",
|
||||||
loadingMore
|
|
||||||
}: ListViewProps<T>) {
|
}: 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 (
|
return (
|
||||||
<div className="w-full flex flex-col items-center">
|
<div className="w-full min-h-screen bg-gray-50 p-6 text-black flex flex-col items-center">
|
||||||
{/* Items */}
|
<div className="w-full sm:w-4/5 flex gap-4 mb-8">
|
||||||
|
<input
|
||||||
|
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
|
||||||
|
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
|
<div
|
||||||
className={`w-full sm:w-4/5 grid gap-6 ${
|
className={`w-full sm:w-4/5 grid gap-6 ${
|
||||||
layout === "square"
|
layout === "square" ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" : "grid-cols-1"
|
||||||
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4"
|
|
||||||
: "grid-cols-1"
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{items.map(item => renderItem(item, layout))}
|
{items.map(item => renderItem(item, layout))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Load More */}
|
{cursorObj && (
|
||||||
{hasMore && (
|
<div className="mt-8 flex justify-center w-full sm:w-4/5">
|
||||||
<div className="mt-8">
|
|
||||||
<button
|
<button
|
||||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
|
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}
|
disabled={loadingMore}
|
||||||
onClick={onLoadMore}
|
|
||||||
>
|
>
|
||||||
{loadingMore ? "Loading..." : "Load More"}
|
{loadingMore ? "Loading..." : "Load More"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{loading && <div className="mt-20 font-medium">Loading...</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
type SearchBarProps = {
|
|
||||||
placeholder?: string;
|
|
||||||
search: string;
|
|
||||||
setSearch: (value: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function SearchBar({
|
|
||||||
placeholder = "Search...",
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
}: SearchBarProps) {
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={search}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="
|
|
||||||
w-full
|
|
||||||
px-4
|
|
||||||
py-2
|
|
||||||
border
|
|
||||||
border-gray-300
|
|
||||||
rounded-lg
|
|
||||||
focus:outline-none
|
|
||||||
focus:ring-2
|
|
||||||
focus:ring-blue-500
|
|
||||||
text-black
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import type { TitleSort } from "../../api";
|
|
||||||
import { ChevronDownIcon, ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
type TitlesSortBoxProps = {
|
|
||||||
sort: TitleSort;
|
|
||||||
setSort: (value: TitleSort) => void;
|
|
||||||
sortForward: boolean;
|
|
||||||
setSortForward: (value: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SORT_OPTIONS: TitleSort[] = ["id", "rating", "year", "views"];
|
|
||||||
|
|
||||||
export function TitlesSortBox({
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
sortForward,
|
|
||||||
setSortForward,
|
|
||||||
}: TitlesSortBoxProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const toggleSortDirection = () => setSortForward(!sortForward);
|
|
||||||
const handleSortSelect = (newSort: TitleSort) => {
|
|
||||||
setSort(newSort);
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="inline-flex relative z-50">
|
|
||||||
{/* Левая часть — смена направления */}
|
|
||||||
<button
|
|
||||||
onClick={toggleSortDirection}
|
|
||||||
className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded-l-lg transition"
|
|
||||||
>
|
|
||||||
{sortForward ? <ArrowUpIcon className="w-4 h-4 mr-1" /> : <ArrowDownIcon className="w-4 h-4 mr-1" />}
|
|
||||||
<span className="text-sm font-medium">Order</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Правая часть — выбор параметра */}
|
|
||||||
<button
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
className="px-4 py-2 flex items-center justify-center bg-gray-100 hover:bg-gray-200 border border-gray-300 border-l-0 rounded-r-lg transition"
|
|
||||||
>
|
|
||||||
<span className="text-sm font-medium">{sort}</span>
|
|
||||||
<ChevronDownIcon className="w-4 h-4 ml-1" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown */}
|
|
||||||
{open && (
|
|
||||||
<ul className="absolute top-full left-0 mt-1 w-40 bg-white border border-gray-300 rounded-md shadow-lg z-[1000]">
|
|
||||||
{SORT_OPTIONS.map(option => (
|
|
||||||
<li key={option}>
|
|
||||||
<button
|
|
||||||
className={`w-full text-left px-4 py-2 hover:bg-gray-100 transition ${
|
|
||||||
option === sort ? "font-bold bg-gray-100" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => handleSortSelect(option)}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,5 +5,4 @@ html, body, #root {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@apply text-black bg-white;
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,154 +1,52 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ListView } from "../../components/ListView/ListView";
|
import { ListView } from "../../components/ListView/ListView";
|
||||||
import { SearchBar } from "../../components/SearchBar/SearchBar";
|
|
||||||
import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox";
|
|
||||||
import { DefaultService } from "../../api/services/DefaultService";
|
import { DefaultService } from "../../api/services/DefaultService";
|
||||||
import { TitleCardSquare } from "../../components/cards/TitleCardSquare";
|
import { TitleCardSquare } from "../../components/cards/TitleCardSquare";
|
||||||
import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal";
|
import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal";
|
||||||
import type { CursorObj, Title, TitleSort } from "../../api";
|
import type { Title } from "../../api";
|
||||||
import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const PAGE_SIZE = 10;
|
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
export default function TitlesPage() {
|
export default function TitlesPage() {
|
||||||
const [titles, setTitles] = useState<Title[]>([]);
|
|
||||||
const [nextPage, setNextPage] = useState<Title[]>([]);
|
|
||||||
const [cursor, setCursor] = useState<CursorObj | null>(null);
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [loadingMore, setLoadingMore] = useState(false);
|
|
||||||
const [sort, setSort] = useState<TitleSort>("id");
|
|
||||||
const [sortForward, setSortForward] = useState(true);
|
|
||||||
const [layout, setLayout] = useState<"square" | "horizontal">("square");
|
|
||||||
|
|
||||||
const fetchPage = async (cursorObj: CursorObj | null) => {
|
const loadTitles = async (cursor: string, limit: number) => {
|
||||||
const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : "";
|
const result = await DefaultService.getTitles(
|
||||||
|
cursor,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
search,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
limit,
|
||||||
|
undefined,
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
return {
|
||||||
const result = await DefaultService.getTitles(
|
items: result.data ?? [],
|
||||||
cursorStr,
|
cursor: result.cursor ?? null,
|
||||||
sort,
|
};
|
||||||
sortForward,
|
|
||||||
search.trim() || undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
PAGE_SIZE,
|
|
||||||
undefined,
|
|
||||||
"all"
|
|
||||||
);
|
|
||||||
|
|
||||||
if ((result === undefined) || !result.data?.length) {
|
|
||||||
return { items: [], nextCursor: null };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
items: result.data ?? [],
|
|
||||||
nextCursor: result.cursor ?? null
|
|
||||||
};
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.status === 204) {
|
|
||||||
return { items: [], nextCursor: null };
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Инициализация: загружаем сразу две страницы
|
|
||||||
useEffect(() => {
|
|
||||||
const initLoad = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setTitles([]);
|
|
||||||
setNextPage([]);
|
|
||||||
setCursor(null);
|
|
||||||
|
|
||||||
const firstPage = await fetchPage(null);
|
|
||||||
const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null };
|
|
||||||
|
|
||||||
setTitles(firstPage.items);
|
|
||||||
setNextPage(secondPage.items);
|
|
||||||
setCursor(secondPage.nextCursor);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
initLoad();
|
|
||||||
}, [search, sort, sortForward]);
|
|
||||||
|
|
||||||
|
|
||||||
const handleLoadMore = async () => {
|
|
||||||
if (nextPage.length === 0) {
|
|
||||||
setLoadingMore(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingMore(true);
|
|
||||||
|
|
||||||
setTitles(prev => [...prev, ...nextPage]);
|
|
||||||
setNextPage([]);
|
|
||||||
|
|
||||||
// Подгружаем следующую страницу с сервера
|
|
||||||
if (cursor) {
|
|
||||||
try {
|
|
||||||
const next = await fetchPage(cursor);
|
|
||||||
if (next.items.length > 0) {
|
|
||||||
setNextPage(next.items);
|
|
||||||
}
|
|
||||||
setCursor(next.nextCursor);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Любой сценарий – выключаем loadingMore
|
|
||||||
setLoadingMore(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-h-screen bg-gray-50 p-6 flex flex-col items-center">
|
<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 text-black">Titles</h1>
|
<h1 className="text-4xl font-bold mb-6 text-center">Titles</h1>
|
||||||
|
|
||||||
<div className="w-full sm:w-4/5 flex flex-col sm:flex-row gap-4 mb-6 items-center">
|
<ListView<Title>
|
||||||
<SearchBar placeholder="Search titles..." search={search} setSearch={setSearch} />
|
pageSize={PAGE_SIZE}
|
||||||
<LayoutSwitch layout={layout} setLayout={setLayout} />
|
fetchItems={loadTitles}
|
||||||
<TitlesSortBox
|
searchPlaceholder="Search titles..."
|
||||||
sort={sort}
|
renderItem={(title, layout) =>
|
||||||
setSort={setSort}
|
layout === "square"
|
||||||
sortForward={sortForward}
|
? <TitleCardSquare title={title} />
|
||||||
setSortForward={setSortForward}
|
: <TitleCardHorizontal title={title} />
|
||||||
/>
|
}
|
||||||
</div>
|
setSearch={setSearch}
|
||||||
|
/>
|
||||||
{loading && <div className="mt-20 font-medium text-black">Loading...</div>}
|
|
||||||
|
|
||||||
{!loading && titles.length === 0 && (
|
|
||||||
<div className="mt-20 font-medium text-black">No titles found.</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{titles.length > 0 && (
|
|
||||||
<>
|
|
||||||
<ListView<Title>
|
|
||||||
items={titles}
|
|
||||||
layout={layout}
|
|
||||||
hasMore={!!cursor || nextPage.length > 1}
|
|
||||||
loadingMore={loadingMore}
|
|
||||||
onLoadMore={handleLoadMore}
|
|
||||||
renderItem={(title, layout) =>
|
|
||||||
layout === "square"
|
|
||||||
? <TitleCardSquare title={title} />
|
|
||||||
: <TitleCardHorizontal title={title} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!cursor && nextPage.length == 0 && (
|
|
||||||
<div className="mt-6 font-medium text-black">
|
|
||||||
Результатов больше нет, было найдено {titles.length} тайтлов.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
||||||
tailwindcss()
|
tailwindcss()
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '127.0.0.1',
|
||||||
port: 8083,
|
port: 8083,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue