mirror of
https://github.com/AnimeThemes/animethemes-web.git
synced 2026-07-11 01:24:31 +02:00
feat: Featured theme is now fetched from the API (#124)
* Added theme table to theme summary card. * Added hotkey to go directly to the search. * Added artist table to theme summary cards in search context. * Added global error boundary. * Added alternative labels for "Add to Playlist" button on mobile. * Improved performance of filtering on anime pages. * Improved performance of the mobile navigation. * Improved performance and stability of search. * Extracted theme table into its own component. * Disabled manual re-ordering of local playlist entries when sorting. * Fixed duplicate toasts. * Fixed issue with z-index on the video player. * Fixed sorting of artists on multiple pages. * Fixed multiple issues when sorting the local playlist. * Fixed several console warnings. * Updated Next.js to 12.1.6.
This commit is contained in:
Generated
+137
-131
@@ -25,7 +25,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.0.12",
|
||||
"next": "^12.1.5",
|
||||
"next": "^12.1.6",
|
||||
"p-limit": "^3.1.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"react": "^17.0.2",
|
||||
@@ -523,9 +523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.5.tgz",
|
||||
"integrity": "sha512-+34yUJslfJi7Lyx6ELuN8nWcOzi27izfYnZIC1Dqv7kmmfiBVxgzR3BXhlvEMTKC2IRJhXVs2FkMY+buQe3k7Q=="
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.6.tgz",
|
||||
"integrity": "sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA=="
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "12.0.8",
|
||||
@@ -537,9 +537,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-android-arm-eabi": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.5.tgz",
|
||||
"integrity": "sha512-SKnGTdYcoN04Y2DvE0/Y7/MjkA+ltsmbuH/y/hR7Ob7tsj+8ZdOYuk+YvW1B8dY20nDPHP58XgDTSm2nA8BzzA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz",
|
||||
"integrity": "sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -552,9 +552,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-android-arm64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.5.tgz",
|
||||
"integrity": "sha512-YXiqgQ/9Rxg1dXp6brXbeQM1JDx9SwUY/36JiE+36FXqYEmDYbxld9qkX6GEzkc5rbwJ+RCitargnzEtwGW0mw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz",
|
||||
"integrity": "sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -567,9 +567,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz",
|
||||
"integrity": "sha512-y8mhldb/WFZ6lFeowkGfi0cO/lBdiBqDk4T4LZLvCpoQp4Or/NzUN6P5NzBQZ5/b4oUHM/wQICEM+1wKA4qIVw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz",
|
||||
"integrity": "sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -582,9 +582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.5.tgz",
|
||||
"integrity": "sha512-wqJ3X7WQdTwSGi0kIDEmzw34QHISRIQ5uvC+VXmsIlCPFcMA+zM5723uh8NfuKGquDMiEMS31a83QgkuHMYbwQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz",
|
||||
"integrity": "sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -597,9 +597,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm-gnueabihf": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.5.tgz",
|
||||
"integrity": "sha512-WnhdM5duONMvt2CncAl+9pim0wBxDS2lHoo7ub/o/i1bRbs11UTzosKzEXVaTDCUkCX2c32lIDi1WcN2ZPkcdw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz",
|
||||
"integrity": "sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -612,9 +612,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.5.tgz",
|
||||
"integrity": "sha512-Jq2H68yQ4bLUhR/XQnbw3LDW0GMQn355qx6rU36BthDLeGue7YV7MqNPa8GKvrpPocEMW77nWx/1yI6w6J07gw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz",
|
||||
"integrity": "sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -627,9 +627,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.5.tgz",
|
||||
"integrity": "sha512-KgPjwdbhDqXI7ghNN8V/WAiLquc9Ebe8KBrNNEL0NQr+yd9CyKJ6KqjayVkmX+hbHzbyvbui/5wh/p3CZQ9xcQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz",
|
||||
"integrity": "sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -642,9 +642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.5.tgz",
|
||||
"integrity": "sha512-O2ErUTvCJ6DkNTSr9pbu1n3tcqykqE/ebty1rwClzIYdOgpB3T2MfEPP+K7GhUR87wmN/hlihO9ch7qpVFDGKw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz",
|
||||
"integrity": "sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -657,9 +657,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.5.tgz",
|
||||
"integrity": "sha512-1eIlZmlO/VRjxxzUBcVosf54AFU3ltAzHi+BJA+9U/lPxCYIsT+R4uO3QksRzRjKWhVQMRjEnlXyyq5SKJm7BA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz",
|
||||
"integrity": "sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -672,9 +672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-oromsfokbEuVb0CBLLE7R9qX3KGXucZpsojLpzUh1QJjuy1QkrPJncwr8xmWQnwgtQ6ecMWXgXPB+qtvizT9Tw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -687,9 +687,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-a/51L5KzBpeZSW9LbekMo3I3Cwul+V+QKwbEIMA+Qwb2qrlcn1L9h3lt8cHqNTFt2y72ce6aTwDTw1lyi5oIRA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -702,9 +702,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-/SoXW1Ntpmpw3AXAzfDRaQidnd8kbZ2oSni8u5z0yw6t4RwJvmdZy1eOaAADRThWKV+2oU90++LSnXJIwBRWYQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1391,13 +1391,19 @@
|
||||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001311",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz",
|
||||
"integrity": "sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
"version": "1.0.30001344",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz",
|
||||
"integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
@@ -3147,14 +3153,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-12.1.5.tgz",
|
||||
"integrity": "sha512-YGHDpyfgCfnT5GZObsKepmRnne7Kzp7nGrac07dikhutWQug7hHg85/+sPJ4ZW5Q2pDkb+n0FnmLkmd44htIJQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-12.1.6.tgz",
|
||||
"integrity": "sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==",
|
||||
"dependencies": {
|
||||
"@next/env": "12.1.5",
|
||||
"caniuse-lite": "^1.0.30001283",
|
||||
"@next/env": "12.1.6",
|
||||
"caniuse-lite": "^1.0.30001332",
|
||||
"postcss": "8.4.5",
|
||||
"styled-jsx": "5.0.1"
|
||||
"styled-jsx": "5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
@@ -3163,18 +3169,18 @@
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-android-arm-eabi": "12.1.5",
|
||||
"@next/swc-android-arm64": "12.1.5",
|
||||
"@next/swc-darwin-arm64": "12.1.5",
|
||||
"@next/swc-darwin-x64": "12.1.5",
|
||||
"@next/swc-linux-arm-gnueabihf": "12.1.5",
|
||||
"@next/swc-linux-arm64-gnu": "12.1.5",
|
||||
"@next/swc-linux-arm64-musl": "12.1.5",
|
||||
"@next/swc-linux-x64-gnu": "12.1.5",
|
||||
"@next/swc-linux-x64-musl": "12.1.5",
|
||||
"@next/swc-win32-arm64-msvc": "12.1.5",
|
||||
"@next/swc-win32-ia32-msvc": "12.1.5",
|
||||
"@next/swc-win32-x64-msvc": "12.1.5"
|
||||
"@next/swc-android-arm-eabi": "12.1.6",
|
||||
"@next/swc-android-arm64": "12.1.6",
|
||||
"@next/swc-darwin-arm64": "12.1.6",
|
||||
"@next/swc-darwin-x64": "12.1.6",
|
||||
"@next/swc-linux-arm-gnueabihf": "12.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "12.1.6",
|
||||
"@next/swc-linux-arm64-musl": "12.1.6",
|
||||
"@next/swc-linux-x64-gnu": "12.1.6",
|
||||
"@next/swc-linux-x64-musl": "12.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "12.1.6",
|
||||
"@next/swc-win32-ia32-msvc": "12.1.6",
|
||||
"@next/swc-win32-x64-msvc": "12.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"fibers": ">= 3.1.0",
|
||||
@@ -4110,9 +4116,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/styled-jsx": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz",
|
||||
"integrity": "sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.2.tgz",
|
||||
"integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==",
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
@@ -4960,9 +4966,9 @@
|
||||
}
|
||||
},
|
||||
"@next/env": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.5.tgz",
|
||||
"integrity": "sha512-+34yUJslfJi7Lyx6ELuN8nWcOzi27izfYnZIC1Dqv7kmmfiBVxgzR3BXhlvEMTKC2IRJhXVs2FkMY+buQe3k7Q=="
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-12.1.6.tgz",
|
||||
"integrity": "sha512-Te/OBDXFSodPU6jlXYPAXpmZr/AkG6DCATAxttQxqOWaq6eDFX25Db3dK0120GZrSZmv4QCe9KsZmJKDbWs4OA=="
|
||||
},
|
||||
"@next/eslint-plugin-next": {
|
||||
"version": "12.0.8",
|
||||
@@ -4974,75 +4980,75 @@
|
||||
}
|
||||
},
|
||||
"@next/swc-android-arm-eabi": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.5.tgz",
|
||||
"integrity": "sha512-SKnGTdYcoN04Y2DvE0/Y7/MjkA+ltsmbuH/y/hR7Ob7tsj+8ZdOYuk+YvW1B8dY20nDPHP58XgDTSm2nA8BzzA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.1.6.tgz",
|
||||
"integrity": "sha512-BxBr3QAAAXWgk/K7EedvzxJr2dE014mghBSA9iOEAv0bMgF+MRq4PoASjuHi15M2zfowpcRG8XQhMFtxftCleQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-android-arm64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.5.tgz",
|
||||
"integrity": "sha512-YXiqgQ/9Rxg1dXp6brXbeQM1JDx9SwUY/36JiE+36FXqYEmDYbxld9qkX6GEzkc5rbwJ+RCitargnzEtwGW0mw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.1.6.tgz",
|
||||
"integrity": "sha512-EboEk3ROYY7U6WA2RrMt/cXXMokUTXXfnxe2+CU+DOahvbrO8QSWhlBl9I9ZbFzJx28AGB9Yo3oQHCvph/4Lew==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-darwin-arm64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.5.tgz",
|
||||
"integrity": "sha512-y8mhldb/WFZ6lFeowkGfi0cO/lBdiBqDk4T4LZLvCpoQp4Or/NzUN6P5NzBQZ5/b4oUHM/wQICEM+1wKA4qIVw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.1.6.tgz",
|
||||
"integrity": "sha512-P0EXU12BMSdNj1F7vdkP/VrYDuCNwBExtRPDYawgSUakzi6qP0iKJpya2BuLvNzXx+XPU49GFuDC5X+SvY0mOw==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-darwin-x64": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.5.tgz",
|
||||
"integrity": "sha512-wqJ3X7WQdTwSGi0kIDEmzw34QHISRIQ5uvC+VXmsIlCPFcMA+zM5723uh8NfuKGquDMiEMS31a83QgkuHMYbwQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.1.6.tgz",
|
||||
"integrity": "sha512-9FptMnbgHJK3dRDzfTpexs9S2hGpzOQxSQbe8omz6Pcl7rnEp9x4uSEKY51ho85JCjL4d0tDLBcXEJZKKLzxNg==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm-gnueabihf": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.5.tgz",
|
||||
"integrity": "sha512-WnhdM5duONMvt2CncAl+9pim0wBxDS2lHoo7ub/o/i1bRbs11UTzosKzEXVaTDCUkCX2c32lIDi1WcN2ZPkcdw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.1.6.tgz",
|
||||
"integrity": "sha512-PvfEa1RR55dsik/IDkCKSFkk6ODNGJqPY3ysVUZqmnWMDSuqFtf7BPWHFa/53znpvVB5XaJ5Z1/6aR5CTIqxPw==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm64-gnu": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.5.tgz",
|
||||
"integrity": "sha512-Jq2H68yQ4bLUhR/XQnbw3LDW0GMQn355qx6rU36BthDLeGue7YV7MqNPa8GKvrpPocEMW77nWx/1yI6w6J07gw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.1.6.tgz",
|
||||
"integrity": "sha512-53QOvX1jBbC2ctnmWHyRhMajGq7QZfl974WYlwclXarVV418X7ed7o/EzGY+YVAEKzIVaAB9JFFWGXn8WWo0gQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-arm64-musl": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.5.tgz",
|
||||
"integrity": "sha512-KgPjwdbhDqXI7ghNN8V/WAiLquc9Ebe8KBrNNEL0NQr+yd9CyKJ6KqjayVkmX+hbHzbyvbui/5wh/p3CZQ9xcQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.1.6.tgz",
|
||||
"integrity": "sha512-CMWAkYqfGdQCS+uuMA1A2UhOfcUYeoqnTW7msLr2RyYAys15pD960hlDfq7QAi8BCAKk0sQ2rjsl0iqMyziohQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-x64-gnu": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.5.tgz",
|
||||
"integrity": "sha512-O2ErUTvCJ6DkNTSr9pbu1n3tcqykqE/ebty1rwClzIYdOgpB3T2MfEPP+K7GhUR87wmN/hlihO9ch7qpVFDGKw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.1.6.tgz",
|
||||
"integrity": "sha512-AC7jE4Fxpn0s3ujngClIDTiEM/CQiB2N2vkcyWWn6734AmGT03Duq6RYtPMymFobDdAtZGFZd5nR95WjPzbZAQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-linux-x64-musl": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.5.tgz",
|
||||
"integrity": "sha512-1eIlZmlO/VRjxxzUBcVosf54AFU3ltAzHi+BJA+9U/lPxCYIsT+R4uO3QksRzRjKWhVQMRjEnlXyyq5SKJm7BA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.1.6.tgz",
|
||||
"integrity": "sha512-c9Vjmi0EVk0Kou2qbrynskVarnFwfYIi+wKufR9Ad7/IKKuP6aEhOdZiIIdKsYWRtK2IWRF3h3YmdnEa2WLUag==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-arm64-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-oromsfokbEuVb0CBLLE7R9qX3KGXucZpsojLpzUh1QJjuy1QkrPJncwr8xmWQnwgtQ6ecMWXgXPB+qtvizT9Tw==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-3UTOL/5XZSKFelM7qN0it35o3Cegm6LsyuERR3/OoqEExyj3aCk7F025b54/707HTMAnjlvQK3DzLhPu/xxO4g==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-ia32-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-a/51L5KzBpeZSW9LbekMo3I3Cwul+V+QKwbEIMA+Qwb2qrlcn1L9h3lt8cHqNTFt2y72ce6aTwDTw1lyi5oIRA==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-8ZWoj6nCq6fI1yCzKq6oK0jE6Mxlz4MrEsRyu0TwDztWQWe7rh4XXGLAa2YVPatYcHhMcUL+fQQbqd1MsgaSDA==",
|
||||
"optional": true
|
||||
},
|
||||
"@next/swc-win32-x64-msvc": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.5.tgz",
|
||||
"integrity": "sha512-/SoXW1Ntpmpw3AXAzfDRaQidnd8kbZ2oSni8u5z0yw6t4RwJvmdZy1eOaAADRThWKV+2oU90++LSnXJIwBRWYQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.1.6.tgz",
|
||||
"integrity": "sha512-4ZEwiRuZEicXhXqmhw3+de8Z4EpOLQj/gp+D9fFWo6ii6W1kBkNNvvEx4A90ugppu+74pT1lIJnOuz3A9oQeJA==",
|
||||
"optional": true
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
@@ -5539,9 +5545,9 @@
|
||||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001311",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001311.tgz",
|
||||
"integrity": "sha512-mleTFtFKfykEeW34EyfhGIFjGCqzhh38Y0LhdQ9aWF+HorZTtdgKV/1hEE0NlFkG2ubvisPV6l400tlbPys98A=="
|
||||
"version": "1.0.30001344",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001344.tgz",
|
||||
"integrity": "sha512-0ZFjnlCaXNOAYcV7i+TtdKBp0L/3XEU2MF/x6Du1lrh+SRX4IfzIVL4HNJg5pB2PmFb8rszIGyOvsZnqqRoc2g=="
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
@@ -6842,26 +6848,26 @@
|
||||
"dev": true
|
||||
},
|
||||
"next": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-12.1.5.tgz",
|
||||
"integrity": "sha512-YGHDpyfgCfnT5GZObsKepmRnne7Kzp7nGrac07dikhutWQug7hHg85/+sPJ4ZW5Q2pDkb+n0FnmLkmd44htIJQ==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-12.1.6.tgz",
|
||||
"integrity": "sha512-cebwKxL3/DhNKfg9tPZDQmbRKjueqykHHbgaoG4VBRH3AHQJ2HO0dbKFiS1hPhe1/qgc2d/hFeadsbPicmLD+A==",
|
||||
"requires": {
|
||||
"@next/env": "12.1.5",
|
||||
"@next/swc-android-arm-eabi": "12.1.5",
|
||||
"@next/swc-android-arm64": "12.1.5",
|
||||
"@next/swc-darwin-arm64": "12.1.5",
|
||||
"@next/swc-darwin-x64": "12.1.5",
|
||||
"@next/swc-linux-arm-gnueabihf": "12.1.5",
|
||||
"@next/swc-linux-arm64-gnu": "12.1.5",
|
||||
"@next/swc-linux-arm64-musl": "12.1.5",
|
||||
"@next/swc-linux-x64-gnu": "12.1.5",
|
||||
"@next/swc-linux-x64-musl": "12.1.5",
|
||||
"@next/swc-win32-arm64-msvc": "12.1.5",
|
||||
"@next/swc-win32-ia32-msvc": "12.1.5",
|
||||
"@next/swc-win32-x64-msvc": "12.1.5",
|
||||
"caniuse-lite": "^1.0.30001283",
|
||||
"@next/env": "12.1.6",
|
||||
"@next/swc-android-arm-eabi": "12.1.6",
|
||||
"@next/swc-android-arm64": "12.1.6",
|
||||
"@next/swc-darwin-arm64": "12.1.6",
|
||||
"@next/swc-darwin-x64": "12.1.6",
|
||||
"@next/swc-linux-arm-gnueabihf": "12.1.6",
|
||||
"@next/swc-linux-arm64-gnu": "12.1.6",
|
||||
"@next/swc-linux-arm64-musl": "12.1.6",
|
||||
"@next/swc-linux-x64-gnu": "12.1.6",
|
||||
"@next/swc-linux-x64-musl": "12.1.6",
|
||||
"@next/swc-win32-arm64-msvc": "12.1.6",
|
||||
"@next/swc-win32-ia32-msvc": "12.1.6",
|
||||
"@next/swc-win32-x64-msvc": "12.1.6",
|
||||
"caniuse-lite": "^1.0.30001332",
|
||||
"postcss": "8.4.5",
|
||||
"styled-jsx": "5.0.1"
|
||||
"styled-jsx": "5.0.2"
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
@@ -7510,9 +7516,9 @@
|
||||
}
|
||||
},
|
||||
"styled-jsx": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.1.tgz",
|
||||
"integrity": "sha512-+PIZ/6Uk40mphiQJJI1202b+/dYeTVd9ZnMPR80pgiWbjIwvN2zIp4r9et0BgqBuShh48I0gttPlAXA7WVvBxw==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.0.2.tgz",
|
||||
"integrity": "sha512-LqPQrbBh3egD57NBcHET4qcgshPks+yblyhPlH2GY8oaDgKs8SK4C3dBh3oSJjgzJ3G5t1SYEZGHkP+QEpX9EQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"supports-color": {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.0.12",
|
||||
"next": "^12.1.5",
|
||||
"next": "^12.1.6",
|
||||
"p-limit": "^3.1.0",
|
||||
"prismjs": "^1.27.0",
|
||||
"react": "^17.0.2",
|
||||
|
||||
@@ -7,7 +7,7 @@ const Flex = styled.div`
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-wrap: ${(props) => props.wrap && "wrap"};
|
||||
flex-wrap: ${(props) => props.$wrap && "wrap"};
|
||||
|
||||
justify-content: var(--justify-content);
|
||||
align-items: var(--align-items);
|
||||
|
||||
@@ -4,20 +4,16 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "components/button";
|
||||
import { Text } from "components/text";
|
||||
import useImage from "hooks/useImage";
|
||||
import createVideoSlug from "utils/createVideoSlug";
|
||||
import { Icon } from "components/icon";
|
||||
import { SummaryCard } from "components/card";
|
||||
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||
import styled from "styled-components";
|
||||
import useToggle from "hooks/useToggle";
|
||||
import theme from "theme";
|
||||
import gql from "graphql-tag";
|
||||
import { uniq } from "lodash-es";
|
||||
import { Collapse, SongTitle, ThemeEntryTags, VideoTags } from "components/utils";
|
||||
import { Table } from "components/table";
|
||||
import { Collapse } from "components/utils";
|
||||
import { ThemeTable } from "components/table";
|
||||
import useMediaQuery from "hooks/useMediaQuery";
|
||||
import { ContentWarningTags, EpisodeTag } from "components/tag";
|
||||
import { Row } from "components/box";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative
|
||||
@@ -133,7 +129,7 @@ export function AnimeSummaryCard({ anime, previewThemes = false, expandable = fa
|
||||
{!!group && (
|
||||
<Text variant="h2">{group}</Text>
|
||||
)}
|
||||
<ThemesTable anime={anime} group={group}/>
|
||||
<ThemeTable themes={anime.themes.filter((theme) => theme.group === group)}/>
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledThemeGroupContainer>
|
||||
@@ -143,66 +139,6 @@ export function AnimeSummaryCard({ anime, previewThemes = false, expandable = fa
|
||||
);
|
||||
}
|
||||
|
||||
function ThemesTable({ anime, group = null }) {
|
||||
const rows = anime.themes
|
||||
.filter((theme) => theme.group === group && "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
|
||||
.sort(chain(themeTypeComparator, themeIndexComparator))
|
||||
.map((theme) => theme.entries.map((entry, entryIndex) => entry.videos.map((video, videoIndex) => {
|
||||
const videoSlug = createVideoSlug(theme, entry, video);
|
||||
|
||||
return (
|
||||
<Link key={anime.slug + videoSlug} href={`/anime/${anime.slug}/${videoSlug}`} passHref prefetch={false}>
|
||||
<Table.Row as="a">
|
||||
<Table.Cell>
|
||||
{!videoIndex && (
|
||||
entry.version > 1 ? (
|
||||
<Text variant="small" color="text-muted">{theme.type}{theme.sequence || null} v{entry.version}</Text>
|
||||
) : (
|
||||
<Text variant="small">{theme.type}{theme.sequence || null}</Text>
|
||||
)
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{(!entryIndex && !videoIndex) && (
|
||||
<SongTitle song={theme.song}/>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{!videoIndex && (
|
||||
<EpisodeTag entry={entry}/>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{!videoIndex && (
|
||||
<Row wrap style={{ "--gap": "8px", "--align-items": "baseline" }}>
|
||||
<ContentWarningTags entry={entry}/>
|
||||
</Row>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<VideoTags video={video}/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Link>
|
||||
);
|
||||
})));
|
||||
|
||||
return (
|
||||
<Table style={{ "--columns": "42px 3fr 2fr 2fr 2fr" }}>
|
||||
<Table.Head>
|
||||
<Table.HeadCell>Type</Table.HeadCell>
|
||||
<Table.HeadCell>Song Title</Table.HeadCell>
|
||||
<Table.HeadCell>Episode Count</Table.HeadCell>
|
||||
<Table.HeadCell>Content Warning</Table.HeadCell>
|
||||
<Table.HeadCell>Notes</Table.HeadCell>
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
{rows}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
AnimeSummaryCard.fragments = {
|
||||
anime: gql`
|
||||
${useImage.fragment}
|
||||
@@ -215,53 +151,13 @@ AnimeSummaryCard.fragments = {
|
||||
season
|
||||
}
|
||||
`,
|
||||
previewThemes: gql`
|
||||
${createVideoSlug.fragments.theme}
|
||||
${createVideoSlug.fragments.entry}
|
||||
${createVideoSlug.fragments.video}
|
||||
|
||||
fragment AnimeSummaryCard_anime_previewThemes on Anime {
|
||||
themes {
|
||||
...createVideoSlug_theme
|
||||
slug
|
||||
group
|
||||
type
|
||||
sequence
|
||||
entries {
|
||||
...createVideoSlug_entry
|
||||
videos {
|
||||
...createVideoSlug_video
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
expandable: gql`
|
||||
${createVideoSlug.fragments.theme}
|
||||
${createVideoSlug.fragments.entry}
|
||||
${createVideoSlug.fragments.video}
|
||||
${VideoTags.fragment}
|
||||
${ThemeEntryTags.fragment}
|
||||
${ThemeTable.fragments.theme}
|
||||
|
||||
fragment AnimeSummaryCard_anime_expandable on Anime {
|
||||
themes {
|
||||
...createVideoSlug_theme
|
||||
slug
|
||||
...ThemeTable_theme
|
||||
group
|
||||
type
|
||||
sequence
|
||||
entries {
|
||||
...createVideoSlug_entry
|
||||
...ThemeEntryTags_entry
|
||||
version
|
||||
videos {
|
||||
...createVideoSlug_video
|
||||
...VideoTags_video
|
||||
}
|
||||
}
|
||||
song {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -58,7 +58,7 @@ export function SummaryCard({ title, description, image, to, children, ...props
|
||||
</a>
|
||||
</Link>
|
||||
<StyledBody>
|
||||
<Text maxLines={1} title={typeof title === "string" && title}>
|
||||
<Text maxLines={1} title={typeof title === "string" ? title : undefined}>
|
||||
{typeof title === "string" ? (
|
||||
<Link href={to} passHref prefetch={false}>
|
||||
<Text as="a" link>{title}</Text>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
StyledVideoListContainer
|
||||
} from "./ThemeDetailCard.style";
|
||||
import { VideoButton } from "components/button";
|
||||
import { SongTitleWithArtists, ThemeEntryTags } from "components/utils";
|
||||
import { Performances, SongTitle, ThemeEntryTags } from "components/utils";
|
||||
import { Text } from "components/text";
|
||||
import { ThemeMenu } from "components/menu";
|
||||
|
||||
@@ -15,7 +15,10 @@ export function ThemeDetailCard({ theme }) {
|
||||
<StyledThemeCard>
|
||||
<StyledRow>
|
||||
<StyledSequence>{theme.type}{theme.sequence || null}</StyledSequence>
|
||||
<SongTitleWithArtists song={theme.song}/>
|
||||
<Text>
|
||||
<SongTitle song={theme.song}/>
|
||||
<Performances song={theme.song} expandable/>
|
||||
</Text>
|
||||
<ThemeMenu theme={theme}/>
|
||||
</StyledRow>
|
||||
{theme.entries.map(entry => (
|
||||
|
||||
@@ -12,7 +12,7 @@ import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "components/button";
|
||||
import useToggle from "hooks/useToggle";
|
||||
import styled from "styled-components";
|
||||
import { Table } from "components/table";
|
||||
import { Table, ThemeTable } from "components/table";
|
||||
import theme from "theme";
|
||||
import useMediaQuery from "hooks/useMediaQuery";
|
||||
|
||||
@@ -45,15 +45,16 @@ const StyledExpandButton = styled(Button)`
|
||||
`;
|
||||
|
||||
const StyledPerformedWith = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const useIsMobile = () => useMediaQuery(`(max-width: ${theme.breakpoints.mobileMax})`);
|
||||
|
||||
// Specify an artist if you want to display this in an artist context (e.g. artist page)
|
||||
export function ThemeSummaryCard({ theme, artist, children, expandable: expandableFromContext = false, ...props }) {
|
||||
const expandable = expandableFromContext && !!artist && theme.song?.performances?.length > 1;
|
||||
|
||||
export function ThemeSummaryCard({ theme, artist, children, expandable = false, ...props }) {
|
||||
const [isExpanded, toggleExpanded] = useToggle();
|
||||
const { smallCover } = useImage(theme.anime);
|
||||
const isMobile = useIsMobile();
|
||||
@@ -119,24 +120,30 @@ export function ThemeSummaryCard({ theme, artist, children, expandable: expandab
|
||||
{expandable && (
|
||||
<Collapse collapse={!isExpanded}>
|
||||
<StyledPerformedWith>
|
||||
<Table style={{ "--columns": "1fr" }}>
|
||||
<Table.Head>
|
||||
<Table.HeadCell>Performed With</Table.HeadCell>
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
{theme.song?.performances?.filter((performance) => performance.artist.slug !== artist?.slug).map((performance) => (
|
||||
<Link key={performance.artist.slug} href={`/artist/${performance.artist.slug}`} passHref prefetch={false}>
|
||||
<Table.Row as="a">
|
||||
<Table.Cell>
|
||||
<Text color="text-primary" weight="600">
|
||||
{performance.as ? `${performance.as} (CV: ${performance.artist.name})` : performance.artist.name}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Link>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<ThemeTable themes={[theme]}/>
|
||||
{theme.song?.performances?.length > (artist ? 1 : 0) && (
|
||||
<Table style={{ "--columns": "1fr" }}>
|
||||
<Table.Head>
|
||||
<Table.HeadCell>Performed {artist ? "With" : "By"}</Table.HeadCell>
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
{theme.song?.performances
|
||||
?.filter((performance) => performance.artist.slug !== artist?.slug)
|
||||
.sort((a, b) => a.artist.name.localeCompare(b.artist.name))
|
||||
.map((performance) => (
|
||||
<Link key={performance.artist.slug} href={`/artist/${performance.artist.slug}`} passHref prefetch={false}>
|
||||
<Table.Row as="a">
|
||||
<Table.Cell>
|
||||
<Text color="text-primary" weight="600">
|
||||
{performance.as ? `${performance.as} (CV: ${performance.artist.name})` : performance.artist.name}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Link>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
)}
|
||||
</StyledPerformedWith>
|
||||
</Collapse>
|
||||
)}
|
||||
@@ -182,6 +189,13 @@ ThemeSummaryCard.fragments = {
|
||||
fragment ThemeSummaryCard_artist on Artist {
|
||||
...SongTitleWithArtists_artist
|
||||
}
|
||||
`,
|
||||
expandable: gql`
|
||||
${ThemeTable.fragments.theme}
|
||||
|
||||
fragment ThemeSummaryCard_theme_expandable on Theme {
|
||||
...ThemeTable_theme
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -192,6 +206,10 @@ ThemeSummaryCard.fetchData = async function (id) {
|
||||
query($themeId: Int!) {
|
||||
theme(id: $themeId) {
|
||||
...ThemeSummaryCard_theme
|
||||
anime {
|
||||
year
|
||||
season
|
||||
}
|
||||
}
|
||||
}
|
||||
`, { themeId: id }).then((result) => result.data?.theme);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { Column, Row } from "components/box";
|
||||
import { ThemeDetailCard } from "components/card";
|
||||
import { Listbox } from "components/listbox";
|
||||
import { HorizontalScroll } from "components/utils";
|
||||
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||
|
||||
export function AnimeThemeFilter({ themes }) {
|
||||
export const AnimeThemeFilter = memo(function AnimeThemeFilter({ themes }) {
|
||||
const groups = useMemo(() => themes.reduce((groups, theme) => {
|
||||
const groupName = theme.group || "Original";
|
||||
const group = groups.find((group) => group.name === groupName);
|
||||
@@ -57,4 +57,4 @@ export function AnimeThemeFilter({ themes }) {
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ export function Navigation({ offsetToggleButton = false }) {
|
||||
if (router.pathname !== prevPathname) {
|
||||
setShow(false);
|
||||
setPrevPathname(router.pathname);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,9 @@ export const StyledNavigation = styled(Solid).attrs({ as: "nav" })`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 100vh;
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
${(props) => props.show && css`
|
||||
display: flex;
|
||||
@@ -52,6 +54,7 @@ export const StyledNavigationContainer = styled(Container)`
|
||||
|
||||
background-color: ${theme.colors["solid"]};
|
||||
animation: ${zoomIn} 250ms ease;
|
||||
will-change: transform;
|
||||
|
||||
[theme="dark"] & {
|
||||
border: 2px solid ${theme.colors["text-disabled"]};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Text } from "components/text";
|
||||
import { Input } from "components/input";
|
||||
@@ -46,7 +46,6 @@ export function SearchNavigation() {
|
||||
const { entity, ...query } = router.query;
|
||||
const { q: initialSearchQuery = "" } = query;
|
||||
|
||||
const isReady = useRef(false);
|
||||
const [inputSearchQuery, setInputSearchQuery] = useState("");
|
||||
|
||||
const updateInputSearchQuery = (newInputSearchQuery) => {
|
||||
@@ -55,22 +54,38 @@ export function SearchNavigation() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady.current) {
|
||||
if (router.isReady) {
|
||||
setInputSearchQuery(initialSearchQuery);
|
||||
} else {
|
||||
isReady.current = true;
|
||||
}
|
||||
}, [initialSearchQuery]);
|
||||
}, [router.isReady]);
|
||||
|
||||
const inputRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const onMountInput = useCallback((input) => {
|
||||
// Only focus the input on desktop devices
|
||||
if (window.matchMedia("(hover: hover) and (pointer: fine)").matches) {
|
||||
inputRef.current?.focus({
|
||||
input?.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
}
|
||||
|
||||
inputRef.current = input;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const hotkeyListener = (event) => {
|
||||
if ((event.key === "s" && event.ctrlKey) || (event.key === "/")) {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", hotkeyListener);
|
||||
|
||||
return () => window.removeEventListener("keydown", hotkeyListener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -81,7 +96,7 @@ export function SearchNavigation() {
|
||||
value={inputSearchQuery}
|
||||
onChange={updateInputSearchQuery}
|
||||
inputProps={{
|
||||
ref: inputRef,
|
||||
ref: onMountInput,
|
||||
spellCheck: false,
|
||||
placeholder: "Search"
|
||||
}}
|
||||
|
||||
@@ -9,10 +9,10 @@ export function SearchFilterYear({ value, setValue }) {
|
||||
return (
|
||||
<SearchFilter>
|
||||
<Text variant="h2">Year</Text>
|
||||
<Listbox value={value} onChange={setValue} resettable highlightNonDefault>
|
||||
<Listbox value={value ? String(value) : null} onChange={setValue} resettable highlightNonDefault>
|
||||
<Listbox.Option value={null}>Any</Listbox.Option>
|
||||
{yearList.map((year) => (
|
||||
<Listbox.Option key={year} value={year}>{year}</Listbox.Option>
|
||||
<Listbox.Option key={year} value={year ? String(year) : null}>{year}</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
</SearchFilter>
|
||||
|
||||
@@ -4,43 +4,57 @@ import {
|
||||
SearchFilterSortBy,
|
||||
SearchFilterYear
|
||||
} from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { AnimeSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
import { useState } from "react";
|
||||
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
season: null,
|
||||
year: null,
|
||||
sortBy: null
|
||||
sortBy: "name",
|
||||
};
|
||||
|
||||
export function SearchAnime({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-anime", initialFilter);
|
||||
|
||||
// Use name sort by default if not searching.
|
||||
// If searching and no other sort was selected, use null (= by relevance).
|
||||
const sortBy = searchQuery ? filter.sortBy : (filter.sortBy ?? "name");
|
||||
|
||||
const entitySearch = useEntitySearch("anime", searchQuery, {
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
season: filter.season,
|
||||
year: filter.year
|
||||
},
|
||||
sortBy
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-anime", {
|
||||
...initialFilter,
|
||||
sortBy: searchQuery ? null : initialFilter.sortBy,
|
||||
});
|
||||
const [ prevSearchQuery, setPrevSearchQuery ] = useState(searchQuery);
|
||||
|
||||
if (!searchQuery && filter.sortBy === null) {
|
||||
updateFilter("sortBy")(initialFilter.sortBy);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchQuery !== prevSearchQuery) {
|
||||
// Check if user is switching from non-searching to searching
|
||||
if (searchQuery && !prevSearchQuery) {
|
||||
updateFilter("sortBy")(null);
|
||||
}
|
||||
setPrevSearchQuery(searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchEntity
|
||||
entity="anime"
|
||||
searchQuery={searchQuery}
|
||||
searchParams={{
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
season: filter.season,
|
||||
year: filter.year
|
||||
},
|
||||
sortBy: filter.sortBy
|
||||
}}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSeason value={filter.season} setValue={updateFilter("season")}/>
|
||||
<SearchFilterYear value={filter.year} setValue={updateFilter("year")}/>
|
||||
<SearchFilterSortBy value={sortBy} setValue={updateFilter("sortBy")}>
|
||||
<SearchFilterSortBy value={filter.sortBy} setValue={updateFilter("sortBy")}>
|
||||
{searchQuery ? (
|
||||
<SearchFilterSortBy.Option>Relevance</SearchFilterSortBy.Option>
|
||||
) : null}
|
||||
@@ -52,8 +66,7 @@ export function SearchAnime({ searchQuery }) {
|
||||
</SearchFilterSortBy>
|
||||
</>
|
||||
}
|
||||
renderSummaryCard={(anime) => <AnimeSummaryCard key={anime.slug} anime={anime} expandable/>}
|
||||
{...entitySearch}
|
||||
renderResult={(anime) => <AnimeSummaryCard key={anime.slug} anime={anime} expandable/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { ArtistSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
import { useState } from "react";
|
||||
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: null
|
||||
sortBy: "name",
|
||||
};
|
||||
|
||||
export function SearchArtist({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-artist", initialFilter);
|
||||
|
||||
// Use name sort by default if not searching.
|
||||
// If searching and no other sort was selected, use null (= by relevance).
|
||||
const sortBy = searchQuery ? filter.sortBy : (filter.sortBy ?? "name");
|
||||
|
||||
const entitySearch = useEntitySearch("artist", searchQuery, {
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-artist", {
|
||||
...initialFilter,
|
||||
sortBy: searchQuery ? null : initialFilter.sortBy,
|
||||
});
|
||||
const [ prevSearchQuery, setPrevSearchQuery ] = useState(searchQuery);
|
||||
|
||||
if (!searchQuery && filter.sortBy === null) {
|
||||
updateFilter("sortBy")(initialFilter.sortBy);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchQuery !== prevSearchQuery) {
|
||||
// Check if user is switching from non-searching to searching
|
||||
if (searchQuery && !prevSearchQuery) {
|
||||
updateFilter("sortBy")(null);
|
||||
}
|
||||
setPrevSearchQuery(searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchEntity
|
||||
entity="artist"
|
||||
searchQuery={searchQuery}
|
||||
searchParams={{
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy: filter.sortBy
|
||||
}}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy value={sortBy} setValue={updateFilter("sortBy")}>
|
||||
<SearchFilterSortBy value={filter.sortBy} setValue={updateFilter("sortBy")}>
|
||||
{searchQuery ? (
|
||||
<SearchFilterSortBy.Option>Relevance</SearchFilterSortBy.Option>
|
||||
) : null}
|
||||
@@ -39,8 +53,7 @@ export function SearchArtist({ searchQuery }) {
|
||||
</SearchFilterSortBy>
|
||||
</>
|
||||
}
|
||||
renderSummaryCard={(artist) => <ArtistSummaryCard key={artist.slug} artist={artist}/>}
|
||||
{...entitySearch}
|
||||
renderResult={(artist) => <ArtistSummaryCard key={artist.slug} artist={artist}/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,20 +5,24 @@ import { Column, Row } from "components/box";
|
||||
import { Icon } from "components/icon";
|
||||
import { SearchFilterGroup } from "components/search-filter";
|
||||
import { ErrorCard } from "components/card";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
|
||||
export function SearchEntity({ entity, searchQuery, searchParams, filters, renderResult }) {
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isError,
|
||||
isFetchingNextPage,
|
||||
isLoading,
|
||||
isPlaceholderData,
|
||||
} = useEntitySearch(
|
||||
entity,
|
||||
searchQuery,
|
||||
searchParams,
|
||||
);
|
||||
|
||||
export function SearchEntity({
|
||||
searchQuery,
|
||||
filters,
|
||||
data,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isError,
|
||||
isFetchingNextPage,
|
||||
isLoading,
|
||||
isPlaceholderData,
|
||||
renderSummaryCard
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{!!filters && (
|
||||
@@ -58,7 +62,7 @@ export function SearchEntity({
|
||||
return (
|
||||
<>
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
{results.map(renderSummaryCard)}
|
||||
{results.map(renderResult)}
|
||||
</Column>
|
||||
{(hasNextPage || isPlaceholderData) && (
|
||||
<Row style={{ "--justify-content": "center" }}>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function SearchGlobal({ searchQuery }) {
|
||||
entity="theme"
|
||||
title="Themes"
|
||||
results={themeResults}
|
||||
renderSummaryCard={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme}/>}
|
||||
renderSummaryCard={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme} expandable/>}
|
||||
/>
|
||||
<GlobalSearchSection
|
||||
entity="artist"
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { SummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
import { useState } from "react";
|
||||
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: null
|
||||
sortBy: "name",
|
||||
};
|
||||
|
||||
export function SearchSeries({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-series", initialFilter);
|
||||
|
||||
// Use name sort by default if not searching.
|
||||
// If searching and no other sort was selected, use null (= by relevance).
|
||||
const sortBy = searchQuery ? filter.sortBy : (filter.sortBy ?? "name");
|
||||
|
||||
const entitySearch = useEntitySearch("series", searchQuery, {
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-series", {
|
||||
...initialFilter,
|
||||
sortBy: searchQuery ? null : initialFilter.sortBy,
|
||||
});
|
||||
const [ prevSearchQuery, setPrevSearchQuery ] = useState(searchQuery);
|
||||
|
||||
if (!searchQuery && filter.sortBy === null) {
|
||||
updateFilter("sortBy")(initialFilter.sortBy);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchQuery !== prevSearchQuery) {
|
||||
// Check if user is switching from non-searching to searching
|
||||
if (searchQuery && !prevSearchQuery) {
|
||||
updateFilter("sortBy")(null);
|
||||
}
|
||||
setPrevSearchQuery(searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchEntity
|
||||
entity="series"
|
||||
searchQuery={searchQuery}
|
||||
searchParams={{
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy: filter.sortBy,
|
||||
}}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy value={sortBy} setValue={updateFilter("sortBy")}>
|
||||
<SearchFilterSortBy value={filter.sortBy} setValue={updateFilter("sortBy")}>
|
||||
{searchQuery ? (
|
||||
<SearchFilterSortBy.Option>Relevance</SearchFilterSortBy.Option>
|
||||
) : null}
|
||||
@@ -39,8 +53,7 @@ export function SearchSeries({ searchQuery }) {
|
||||
</SearchFilterSortBy>
|
||||
</>
|
||||
}
|
||||
renderSummaryCard={(series) => <SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />}
|
||||
{...entitySearch}
|
||||
renderResult={(series) => <SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,49 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { SummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
import { useState } from "react";
|
||||
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: null
|
||||
sortBy: "name",
|
||||
};
|
||||
|
||||
export function SearchStudio({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-studio", initialFilter);
|
||||
|
||||
// Use name sort by default if not searching.
|
||||
// If searching and no other sort was selected, use null (= by relevance).
|
||||
const sortBy = searchQuery ? filter.sortBy : (filter.sortBy ?? "name");
|
||||
|
||||
const entitySearch = useEntitySearch("studio", searchQuery, {
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-studio", {
|
||||
...initialFilter,
|
||||
sortBy: searchQuery ? null : initialFilter.sortBy,
|
||||
});
|
||||
const [ prevSearchQuery, setPrevSearchQuery ] = useState(searchQuery);
|
||||
|
||||
if (!searchQuery && filter.sortBy === null) {
|
||||
updateFilter("sortBy")(initialFilter.sortBy);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchQuery !== prevSearchQuery) {
|
||||
// Check if user is switching from non-searching to searching
|
||||
if (searchQuery && !prevSearchQuery) {
|
||||
updateFilter("sortBy")(null);
|
||||
}
|
||||
setPrevSearchQuery(searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchEntity
|
||||
entity="studio"
|
||||
searchQuery={searchQuery}
|
||||
searchParams={{
|
||||
filters: {
|
||||
"name-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
},
|
||||
sortBy: filter.sortBy,
|
||||
}}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy value={sortBy} setValue={updateFilter("sortBy")}>
|
||||
<SearchFilterSortBy value={filter.sortBy} setValue={updateFilter("sortBy")}>
|
||||
{searchQuery ? (
|
||||
<SearchFilterSortBy.Option>Relevance</SearchFilterSortBy.Option>
|
||||
) : null}
|
||||
@@ -39,8 +53,7 @@ export function SearchStudio({ searchQuery }) {
|
||||
</SearchFilterSortBy>
|
||||
</>
|
||||
}
|
||||
renderSummaryCard={(studio) => <SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />}
|
||||
{...entitySearch}
|
||||
renderResult={(studio) => <SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { ThemeSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
import { useState } from "react";
|
||||
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
type: null,
|
||||
sortBy: null
|
||||
sortBy: "song.title",
|
||||
};
|
||||
|
||||
export function SearchTheme({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-theme", initialFilter);
|
||||
|
||||
// Use song.title sort by default if not searching.
|
||||
// If searching and no other sort was selected, use null (= by relevance).
|
||||
const sortBy = searchQuery ? filter.sortBy : (filter.sortBy ?? "song.title");
|
||||
|
||||
const entitySearch = useEntitySearch("theme", searchQuery, {
|
||||
filters: {
|
||||
has: "song",
|
||||
"song][title-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
type: filter.type
|
||||
},
|
||||
sortBy
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter-theme", {
|
||||
...initialFilter,
|
||||
sortBy: searchQuery ? null : initialFilter.sortBy,
|
||||
});
|
||||
const [ prevSearchQuery, setPrevSearchQuery ] = useState(searchQuery);
|
||||
|
||||
if (!searchQuery && filter.sortBy === null) {
|
||||
updateFilter("sortBy")(initialFilter.sortBy);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (searchQuery !== prevSearchQuery) {
|
||||
// Check if user is switching from non-searching to searching
|
||||
if (searchQuery && !prevSearchQuery) {
|
||||
updateFilter("sortBy")(null);
|
||||
}
|
||||
setPrevSearchQuery(searchQuery);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchEntity
|
||||
entity="theme"
|
||||
searchQuery={searchQuery}
|
||||
searchParams={{
|
||||
filters: {
|
||||
has: "song",
|
||||
"song][title-like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
type: filter.type
|
||||
},
|
||||
sortBy: filter.sortBy,
|
||||
}}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterThemeType value={filter.type} setValue={updateFilter("type")}/>
|
||||
<SearchFilterSortBy value={sortBy} setValue={updateFilter("sortBy")}>
|
||||
<SearchFilterSortBy value={filter.sortBy} setValue={updateFilter("sortBy")}>
|
||||
{searchQuery ? (
|
||||
<SearchFilterSortBy.Option>Relevance</SearchFilterSortBy.Option>
|
||||
) : null}
|
||||
@@ -45,8 +59,7 @@ export function SearchTheme({ searchQuery }) {
|
||||
</SearchFilterSortBy>
|
||||
</>
|
||||
}
|
||||
renderSummaryCard={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme}/>}
|
||||
{...entitySearch}
|
||||
renderResult={(theme) => <ThemeSummaryCard key={theme.anime.slug + theme.slug} theme={theme} expandable/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ Table.Row = styled.div`
|
||||
`;
|
||||
|
||||
Table.Cell = styled.div`
|
||||
grid-column: span var(--span, 1);
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||
import createVideoSlug from "utils/createVideoSlug";
|
||||
import Link from "next/link";
|
||||
import { Table } from "components/table";
|
||||
import { Text } from "components/text";
|
||||
import { SongTitle, ThemeEntryTags, VideoTags } from "components/utils";
|
||||
import { ContentWarningTags, EpisodeTag } from "components/tag";
|
||||
import { Row } from "components/box";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export function ThemeTable({ themes }) {
|
||||
const rows = themes
|
||||
.filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
|
||||
.sort(chain(themeTypeComparator, themeIndexComparator))
|
||||
.map((theme) => theme.entries.map((entry, entryIndex) => entry.videos.map((video, videoIndex) => {
|
||||
const videoSlug = createVideoSlug(theme, entry, video);
|
||||
|
||||
return (
|
||||
<Link key={theme.anime.slug + videoSlug} href={`/anime/${theme.anime.slug}/${videoSlug}`} passHref prefetch={false}>
|
||||
<Table.Row as="a">
|
||||
<Table.Cell style={{ "--span": (entryIndex || videoIndex) ? 2 : undefined }}>
|
||||
{!videoIndex && (
|
||||
entry.version > 1 ? (
|
||||
<Text variant="small" color="text-muted">{theme.type}{theme.sequence || null} v{entry.version}</Text>
|
||||
) : (
|
||||
<Text variant="small">{theme.type}{theme.sequence || null}</Text>
|
||||
)
|
||||
)}
|
||||
</Table.Cell>
|
||||
{(!entryIndex && !videoIndex) && (
|
||||
<Table.Cell>
|
||||
<SongTitle song={theme.song}/>
|
||||
</Table.Cell>
|
||||
)}
|
||||
<Table.Cell>
|
||||
{!videoIndex && (
|
||||
<EpisodeTag entry={entry}/>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{!videoIndex && (
|
||||
<Row $wrap style={{ "--gap": "8px", "--align-items": "baseline" }}>
|
||||
<ContentWarningTags entry={entry}/>
|
||||
</Row>
|
||||
)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<VideoTags video={video}/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Link>
|
||||
);
|
||||
})));
|
||||
|
||||
return (
|
||||
<Table style={{ "--columns": "42px 3fr 2fr 2fr 2fr" }}>
|
||||
<Table.Head>
|
||||
<Table.HeadCell>Type</Table.HeadCell>
|
||||
<Table.HeadCell>Song Title</Table.HeadCell>
|
||||
<Table.HeadCell>Episode Count</Table.HeadCell>
|
||||
<Table.HeadCell>Content Warning</Table.HeadCell>
|
||||
<Table.HeadCell>Notes</Table.HeadCell>
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
{rows}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
ThemeTable.fragments = {
|
||||
theme: gql`
|
||||
${createVideoSlug.fragments.theme}
|
||||
${createVideoSlug.fragments.entry}
|
||||
${createVideoSlug.fragments.video}
|
||||
${VideoTags.fragment}
|
||||
${ThemeEntryTags.fragment}
|
||||
|
||||
fragment ThemeTable_theme on Theme {
|
||||
...createVideoSlug_theme
|
||||
type
|
||||
sequence
|
||||
anime {
|
||||
slug
|
||||
}
|
||||
entries {
|
||||
...createVideoSlug_entry
|
||||
...ThemeEntryTags_entry
|
||||
videos {
|
||||
...createVideoSlug_video
|
||||
...VideoTags_video
|
||||
}
|
||||
}
|
||||
song {
|
||||
title
|
||||
}
|
||||
}
|
||||
`
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { Table } from "./Table";
|
||||
export { ThemeTable } from "./ThemeTable";
|
||||
|
||||
@@ -8,7 +8,7 @@ export function PlaylistAddToast({ theme }) {
|
||||
return (
|
||||
<Link href="/profile/playlist" passHref prefetch={false}>
|
||||
<Toast as="a" hoverable>
|
||||
<Row wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<Row $wrap style={{ "--justify-content": "space-between", "--gap": "8px" }}>
|
||||
<span><SongTitle song={theme.song}/> was added to the playlist!</span>
|
||||
<Text color="text-disabled">(Click to view playlist.)</Text>
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Component } from "react";
|
||||
import Link from "next/link";
|
||||
import { Text } from "components/text";
|
||||
import { Container } from "components/container";
|
||||
|
||||
export class ErrorBoundary extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.log({ error, errorInfo });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<Container>
|
||||
<Text variant="h1">Something went wrong.</Text>
|
||||
<Text as="p">
|
||||
<span>You encountered an unhandled error. Please let us know about it on our </span>
|
||||
<Text as="a" link href="https://discordapp.com/invite/m9zbVyQ">Discord</Text>
|
||||
<span>. For now, you may </span>
|
||||
<Link href="/" passHref prefetch={false}>
|
||||
<Text as="a" link>go back to the home page</Text>
|
||||
</Link>
|
||||
<span>.</span>
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { Text } from "components/text";
|
||||
import styled from "styled-components";
|
||||
import { useState } from "react";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
const StyledArtist = styled(Text)`
|
||||
&:not(:first-of-type)::before {
|
||||
@@ -19,7 +20,7 @@ const StyledArtistLink = styled(Text).attrs({ as: "a", link: true })`
|
||||
font-size: 1rem;
|
||||
`;
|
||||
|
||||
export function Performances({ song, artist, maxPerformances = 3 }) {
|
||||
export function Performances({ song, artist, maxPerformances = 3, expandable = false }) {
|
||||
const [expandPerformances, setExpandPerformances] = useState(false);
|
||||
|
||||
if (!song?.performances?.length) {
|
||||
@@ -87,7 +88,7 @@ export function Performances({ song, artist, maxPerformances = 3 }) {
|
||||
))}
|
||||
{!!performancesHidden.length && (
|
||||
<StyledArtist>
|
||||
<Text link onClick={() => setExpandPerformances(true)}>
|
||||
<Text link onClick={() => expandable && setExpandPerformances(true)}>
|
||||
{performancesHidden.length} more
|
||||
</Text>
|
||||
</StyledArtist>
|
||||
@@ -96,3 +97,22 @@ export function Performances({ song, artist, maxPerformances = 3 }) {
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
Performances.fragments = {
|
||||
song: gql`
|
||||
fragment Performances_song on Song {
|
||||
performances {
|
||||
as
|
||||
artist {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
artist: gql`
|
||||
fragment Performances_artist on Artist {
|
||||
slug
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { Text } from "components/text";
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export function SongTitle({ song, songTitleLinkTo }) {
|
||||
const songTitle = song?.title || "T.B.A.";
|
||||
@@ -16,3 +17,11 @@ export function SongTitle({ song, songTitleLinkTo }) {
|
||||
<Text color="text-primary" weight="600" italics={!song?.title}>{songTitle}</Text>
|
||||
);
|
||||
}
|
||||
|
||||
SongTitle.fragments = {
|
||||
song: gql`
|
||||
fragment SongTitle_song on Song {
|
||||
title
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -14,20 +14,19 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
||||
|
||||
SongTitleWithArtists.fragments = {
|
||||
song: gql`
|
||||
${SongTitle.fragments.song}
|
||||
${Performances.fragments.song}
|
||||
|
||||
fragment SongTitleWithArtists_song on Song {
|
||||
title
|
||||
performances {
|
||||
as
|
||||
artist {
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
...SongTitle_song
|
||||
...Performances_song
|
||||
}
|
||||
`,
|
||||
artist: gql`
|
||||
${Performances.fragments.artist}
|
||||
|
||||
fragment SongTitleWithArtists_artist on Artist {
|
||||
slug
|
||||
...Performances_artist
|
||||
}
|
||||
`
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ export { AspectRatio } from "./AspectRatio";
|
||||
export { Collapse } from "./Collapse";
|
||||
export { HeightTransition } from "./HeightTransition";
|
||||
export { SongTitle } from "./SongTitle";
|
||||
export { Performances } from "./Performances";
|
||||
export { SongTitleWithArtists } from "./SongTitleWithArtists";
|
||||
export { ThemeEntryTags } from "./ThemeEntryTags";
|
||||
export { VideoTags } from "./VideoTags";
|
||||
export { HorizontalScroll } from "./HorizontalScroll";
|
||||
export { Highlight } from "./Highlight";
|
||||
export { Performances } from "./Performances";
|
||||
export { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
@@ -77,7 +77,7 @@ export function VideoPlayer({ anime, theme, entry, video, background, ...props }
|
||||
<Column style={{ "--gap": "16px" }}>
|
||||
<Text as="p">Your browser or device doesn't seem to support WebM video which is required to play video files.</Text>
|
||||
<Text as="p">You can try one of the options below to still watch the video:</Text>
|
||||
<Row wrap style={{ "--gap": "16px" }}>
|
||||
<Row $wrap style={{ "--gap": "16px" }}>
|
||||
<IconTextButton
|
||||
variant="solid"
|
||||
forwardedAs="a"
|
||||
@@ -101,7 +101,7 @@ export function VideoPlayer({ anime, theme, entry, video, background, ...props }
|
||||
|
||||
return (
|
||||
<StyledPlayer
|
||||
background={background}
|
||||
$background={background}
|
||||
onDoubleClick={background ? maximize : undefined}
|
||||
onMouseDown={preventTextSelection}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import theme from "theme";
|
||||
import { slideIn } from "styles/animations";
|
||||
|
||||
export const StyledPlayer = styled(motion.div)`
|
||||
${(props) => props.background ? css`
|
||||
z-index: ${theme.zIndices.videoPlayer};
|
||||
|
||||
${(props) => props.$background ? css`
|
||||
position: fixed;
|
||||
z-index: ${theme.zIndices.videoPlayer};
|
||||
|
||||
@media (max-width: 720px) {
|
||||
display: flex;
|
||||
|
||||
@@ -6,21 +6,27 @@ export function ToastProvider({ children, initialToasts = [] }) {
|
||||
const [activeToasts, setActiveToasts] = useState(initialToasts);
|
||||
|
||||
function dispatchToast(id, toast, duration = 5000) {
|
||||
setActiveToasts((prev) => [...prev, { id, content: toast }]);
|
||||
let timeoutId = null;
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
closeToast({ id });
|
||||
}, duration);
|
||||
timeoutId = setTimeout(() => closeToast({ id }), duration);
|
||||
}
|
||||
|
||||
setActiveToasts((prev) => {
|
||||
const duplicates = prev.filter((other) => id === other.id);
|
||||
|
||||
duplicates.forEach((duplicate) => clearTimeout(duplicate.timeoutId));
|
||||
|
||||
const next = prev.filter((other) => id !== other.id);
|
||||
|
||||
next.push({ id, content: toast, timeoutId });
|
||||
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function closeToast({ id }) {
|
||||
setActiveToasts((prev) => {
|
||||
const next = [...prev];
|
||||
next.splice(prev.findIndex((t) => t.id === id), 1);
|
||||
return next;
|
||||
});
|
||||
setActiveToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||
}
|
||||
|
||||
const value = {
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const createKey = (key, field) => `${key}.${field}`;
|
||||
|
||||
export default function useSessionStorage(key, initialData) {
|
||||
const [ sessionStorage, setSessionStorage ] = useState(null);
|
||||
const [ data, setData ] = useState(initialData);
|
||||
|
||||
useEffect(() => {
|
||||
const sessionStorage = window.sessionStorage;
|
||||
|
||||
const loadedData = {};
|
||||
for (const field of Object.keys(initialData)) {
|
||||
const fieldData = sessionStorage.getItem(createKey(key, field));
|
||||
if (fieldData !== null) {
|
||||
loadedData[field] = fieldData;
|
||||
setData((oldData) => {
|
||||
const storageData = sessionStorage.getItem(key);
|
||||
if (storageData !== null) {
|
||||
return { ...oldData, ...JSON.parse(storageData) };
|
||||
}
|
||||
}
|
||||
|
||||
setSessionStorage(sessionStorage);
|
||||
setData((oldData) => ({ ...oldData, ...loadedData }));
|
||||
}, [ key, initialData ]);
|
||||
return oldData;
|
||||
});
|
||||
}, [ key ]);
|
||||
|
||||
const updateDataField = (field) => (newValue) => {
|
||||
if (newValue === initialData[field]) {
|
||||
sessionStorage?.removeItem(createKey(key, field));
|
||||
} else {
|
||||
sessionStorage?.setItem(createKey(key, field), newValue);
|
||||
}
|
||||
const storageData = sessionStorage.getItem(key);
|
||||
const newData = storageData ? JSON.parse(storageData) : { ...initialData };
|
||||
|
||||
setData((oldData) => ({
|
||||
...oldData,
|
||||
[field]: newValue
|
||||
}));
|
||||
newData[field] = newValue;
|
||||
|
||||
sessionStorage.setItem(key, JSON.stringify(newData));
|
||||
|
||||
setData(newData);
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -69,10 +69,20 @@ const entityConfigs = {
|
||||
"group"
|
||||
],
|
||||
animethemeentry: [
|
||||
"version"
|
||||
"version",
|
||||
"episodes",
|
||||
"spoiler",
|
||||
"nsfw"
|
||||
],
|
||||
video: [
|
||||
"tags"
|
||||
"tags",
|
||||
"resolution",
|
||||
"nc",
|
||||
"subbed",
|
||||
"lyrics",
|
||||
"uncen",
|
||||
"source",
|
||||
"overlap"
|
||||
],
|
||||
image: [
|
||||
"facet",
|
||||
@@ -270,6 +280,7 @@ function applyAnimeSchema(allAnime) {
|
||||
delete anime.animethemes;
|
||||
|
||||
for (const theme of anime.themes) {
|
||||
theme.anime = anime;
|
||||
theme.entries = theme.animethemeentries;
|
||||
delete theme.animethemeentries;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,23 @@ const resolvers = {
|
||||
extractor: (result) => result.pages,
|
||||
pagination: true
|
||||
}),
|
||||
counter: () => ({})
|
||||
featuredTheme: apiResolver({
|
||||
endpoint: () => `/config/wiki`,
|
||||
extractor: (result) => result.wiki.featured_theme,
|
||||
transformer: (featuredTheme) => ({
|
||||
animetheme: {
|
||||
...featuredTheme.animethemeentry.animetheme,
|
||||
animethemeentries: [
|
||||
{
|
||||
...featuredTheme.animethemeentry,
|
||||
videos: [
|
||||
featuredTheme.video
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}),
|
||||
},
|
||||
Year: {
|
||||
seasons: apiResolver({
|
||||
@@ -82,15 +98,6 @@ const resolvers = {
|
||||
pagination: true
|
||||
}),
|
||||
},
|
||||
Counter: {
|
||||
anime: () => 0,
|
||||
artist: () => 0,
|
||||
series: () => 0,
|
||||
studio: () => 0,
|
||||
video: () => 0,
|
||||
year: () => 0,
|
||||
season: () => 0,
|
||||
},
|
||||
Anime: {
|
||||
synonyms: apiResolver({
|
||||
endpoint: (anime) => `/anime/${anime.slug}`,
|
||||
@@ -275,6 +282,13 @@ const resolvers = {
|
||||
song: apiResolver({
|
||||
field: "song"
|
||||
}),
|
||||
},
|
||||
FeaturedTheme: {
|
||||
theme: apiResolver({
|
||||
endpoint: (featuredTheme) => `/animetheme/${featuredTheme.animetheme.id}`,
|
||||
field: "animetheme",
|
||||
extractor: (result) => result.animetheme
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const typeDefs = `
|
||||
seasonAll(year: Int): [Season]
|
||||
page(id: Int, slug: String): Page
|
||||
pageAll: [Page]
|
||||
counter: Counter
|
||||
featuredTheme: FeaturedTheme
|
||||
}
|
||||
|
||||
interface ResourceWithImages {
|
||||
@@ -36,16 +36,6 @@ const typeDefs = `
|
||||
anime: [Anime]
|
||||
}
|
||||
|
||||
type Counter {
|
||||
anime: Int!
|
||||
artist: Int!
|
||||
series: Int!
|
||||
studio: Int!
|
||||
video: Int!
|
||||
year: Int!
|
||||
season: Int!
|
||||
}
|
||||
|
||||
type Anime implements ResourceWithImages {
|
||||
id: Int
|
||||
name: String!
|
||||
@@ -173,6 +163,10 @@ const typeDefs = `
|
||||
name: String!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type FeaturedTheme {
|
||||
theme: Theme!
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
|
||||
+18
-2
@@ -9,7 +9,7 @@ import ColorThemeContext from "context/colorThemeContext";
|
||||
import useColorTheme from "hooks/useColorTheme";
|
||||
import { VideoPlayer } from "components/video-player";
|
||||
import PlayerContext from "context/playerContext";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "react-query";
|
||||
import Head from "next/head";
|
||||
import withBasePath from "utils/withBasePath";
|
||||
@@ -23,6 +23,7 @@ import { Text } from "components/text";
|
||||
import { useRouter } from "next/router";
|
||||
import useSetting from "hooks/useSetting";
|
||||
import { DeveloperMode, RevalidationToken } from "utils/settings";
|
||||
import { ErrorBoundary } from "components/utils";
|
||||
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
import "styles/prism.scss";
|
||||
@@ -44,6 +45,7 @@ const StyledContainer = styled(Container)`
|
||||
`;
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
const router = useRouter();
|
||||
const [colorTheme, toggleColorTheme] = useColorTheme();
|
||||
const [developerMode] = useSetting(DeveloperMode);
|
||||
|
||||
@@ -56,6 +58,19 @@ export default function MyApp({ Component, pageProps }) {
|
||||
setLastVideoPageProps(videoPageProps);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const hotkeyListener = (event) => {
|
||||
if (!pageProps.isSearch && ((event.key === "s" && event.ctrlKey) || event.key === "/")) {
|
||||
event.preventDefault();
|
||||
router.push("/search");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", hotkeyListener);
|
||||
|
||||
return () => window.removeEventListener("keydown", hotkeyListener);
|
||||
}, [pageProps.isSearch, router]);
|
||||
|
||||
return (
|
||||
<MultiContextProvider providers={[
|
||||
[ ThemeProvider, { theme } ],
|
||||
@@ -70,7 +85,8 @@ export default function MyApp({ Component, pageProps }) {
|
||||
[ ToastProvider, { initialToasts: [ {
|
||||
id: "announcement",
|
||||
content: <AnnouncementToast/>
|
||||
} ] } ]
|
||||
} ] } ],
|
||||
[ ErrorBoundary ]
|
||||
]}>
|
||||
<GlobalStyle/>
|
||||
<SEO/>
|
||||
|
||||
@@ -82,6 +82,18 @@ const StyledVideoList = styled(Row)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTabletOnly = styled.span`
|
||||
@media (max-width: ${theme.breakpoints.mobileMax}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledMobileOnly = styled.span`
|
||||
@media (min-width: ${theme.breakpoints.tabletMin}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function VideoPage({ anime, theme, entry, video }) {
|
||||
const songTitle = theme.song?.title || "T.B.A.";
|
||||
const { smallCover, largeCover } = useImage(anime);
|
||||
@@ -200,12 +212,18 @@ export default function VideoPage({ anime, theme, entry, video }) {
|
||||
{isInPlaylist(theme) ? (
|
||||
<Button variant="primary" style={{ "--gap": "8px" }} onClick={() => removeFromPlaylist(theme)}>
|
||||
<Icon icon={faMinus}/>
|
||||
<Text>Remove from Playlist</Text>
|
||||
<Text>
|
||||
<StyledTabletOnly>Remove from Playlist</StyledTabletOnly>
|
||||
<StyledMobileOnly>Unsave</StyledMobileOnly>
|
||||
</Text>
|
||||
</Button>
|
||||
) : (
|
||||
<Button style={{ "--gap": "8px" }} onClick={() => addToPlaylist({ ...theme, anime })}>
|
||||
<Icon icon={faPlus}/>
|
||||
<Text>Add to Playlist</Text>
|
||||
<Text>
|
||||
<StyledTabletOnly>Add to Playlist</StyledTabletOnly>
|
||||
<StyledMobileOnly>Save</StyledMobileOnly>
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Menu button={(MenuButton) => (
|
||||
@@ -232,7 +250,7 @@ export default function VideoPage({ anime, theme, entry, video }) {
|
||||
{!!theme.song?.performances?.length && (
|
||||
<>
|
||||
<Text variant="h2">Artists</Text>
|
||||
{theme.song.performances.map((performance) => (
|
||||
{theme.song.performances.sort((a, b) => a.artist.name.localeCompare(b.artist.name)).map((performance) => (
|
||||
<ArtistSummaryCard
|
||||
key={performance.artist.name}
|
||||
artist={performance.artist}
|
||||
|
||||
@@ -183,6 +183,7 @@ export async function getStaticProps({ params: { artistSlug } }) {
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${ThemeSummaryCard.fragments.theme}
|
||||
${ThemeSummaryCard.fragments.artist}
|
||||
${ThemeSummaryCard.fragments.expandable}
|
||||
|
||||
query($artistSlug: String!) {
|
||||
artist(slug: $artistSlug) {
|
||||
@@ -202,6 +203,7 @@ export async function getStaticProps({ params: { artistSlug } }) {
|
||||
}
|
||||
themes {
|
||||
...ThemeSummaryCard_theme
|
||||
...ThemeSummaryCard_theme_expandable
|
||||
id
|
||||
slug
|
||||
group
|
||||
|
||||
@@ -637,12 +637,10 @@ function Color({ color }) {
|
||||
export async function getStaticProps() {
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.previewThemes}
|
||||
|
||||
query {
|
||||
anime(slug: "bakemonogatari") {
|
||||
...AnimeSummaryCard_anime
|
||||
...AnimeSummaryCard_anime_previewThemes
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
+5
-4
@@ -231,11 +231,12 @@ export default function HomePage({ featuredTheme }) {
|
||||
export async function getStaticProps() {
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${FeaturedTheme.fragment}
|
||||
${ThemeSummaryCard.fragments.theme}
|
||||
|
||||
query {
|
||||
theme(id: 11388) {
|
||||
...FeaturedTheme_theme
|
||||
featuredTheme {
|
||||
theme {
|
||||
...FeaturedTheme_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -243,7 +244,7 @@ export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
...getSharedPageProps(apiRequests),
|
||||
featuredTheme: data.theme
|
||||
featuredTheme: data.featuredTheme.theme
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function PlaylistPage() {
|
||||
const [ showFilter, toggleShowFilter ] = useToggle();
|
||||
const [ sortBy, setSortBy ] = useState(UNSORTED);
|
||||
|
||||
const themes = localPlaylist.sort(getComparator(sortBy));
|
||||
const themes = [...localPlaylist].sort(getComparator(sortBy));
|
||||
|
||||
const [ refreshProgress, setRefreshProgess ] = useState(null);
|
||||
const [ refreshError, setRefreshError ] = useState(null);
|
||||
@@ -142,13 +142,22 @@ export default function PlaylistPage() {
|
||||
</Text>
|
||||
</Card>
|
||||
<StyledReorderContainer>
|
||||
<Reorder.Group as="div" axis="y" values={themes} onReorder={setPlaylist}>
|
||||
{themes.map((theme) => (
|
||||
<Reorder.Item as="div" key={theme.anime.slug + theme.slug + theme.group} value={theme}>
|
||||
<ThemeSummaryCard theme={theme}/>
|
||||
</Reorder.Item>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
{sortBy === UNSORTED ? (
|
||||
<Reorder.Group as="div" axis="y" values={themes} onReorder={setPlaylist}>
|
||||
{themes.map((theme) => (
|
||||
<Reorder.Item as="div" key={theme.anime.slug + theme.slug + theme.group} value={theme}>
|
||||
<ThemeSummaryCard theme={theme}/>
|
||||
</Reorder.Item>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
) : (
|
||||
<div>
|
||||
{themes.map((theme) => (
|
||||
<ThemeSummaryCard key={theme.anime.slug + theme.slug + theme.group} theme={theme}/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</StyledReorderContainer>
|
||||
</Column>
|
||||
</SidebarContainer>
|
||||
|
||||
@@ -11,7 +11,9 @@ export default function SearchEntityPage({ entity }) {
|
||||
return (
|
||||
<>
|
||||
<SEO title={`${searchQuery ? `${searchQuery} - ` : ""}${capitalize(entity)} Index`}/>
|
||||
<Index searchEntity={entity} searchQuery={searchQuery}/>
|
||||
{router.isReady ? (
|
||||
<Index searchEntity={entity} searchQuery={searchQuery}/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export default function SeriesDetailPage({ series }) {
|
||||
export async function getStaticProps({ params: { seriesSlug } }) {
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.previewThemes}
|
||||
${AnimeSummaryCard.fragments.expandable}
|
||||
|
||||
query($seriesSlug: String!) {
|
||||
@@ -82,7 +81,6 @@ export async function getStaticProps({ params: { seriesSlug } }) {
|
||||
name
|
||||
anime {
|
||||
...AnimeSummaryCard_anime
|
||||
...AnimeSummaryCard_anime_previewThemes
|
||||
...AnimeSummaryCard_anime_expandable
|
||||
name
|
||||
slug
|
||||
|
||||
@@ -103,7 +103,6 @@ export default function StudioDetailPage({ studio }) {
|
||||
export async function getStaticProps({ params: { studioSlug } }) {
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.previewThemes}
|
||||
${AnimeSummaryCard.fragments.expandable}
|
||||
|
||||
query($studioSlug: String!) {
|
||||
@@ -112,7 +111,6 @@ export async function getStaticProps({ params: { studioSlug } }) {
|
||||
name
|
||||
anime {
|
||||
...AnimeSummaryCard_anime
|
||||
...AnimeSummaryCard_anime_previewThemes
|
||||
...AnimeSummaryCard_anime_expandable
|
||||
name
|
||||
slug
|
||||
|
||||
@@ -36,7 +36,6 @@ export async function getStaticProps({ params: { year, season } }) {
|
||||
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.previewThemes}
|
||||
${AnimeSummaryCard.fragments.expandable}
|
||||
|
||||
query($year: Int = 0, $season: String!) {
|
||||
@@ -50,7 +49,6 @@ export async function getStaticProps({ params: { year, season } }) {
|
||||
anime {
|
||||
slug
|
||||
...AnimeSummaryCard_anime
|
||||
...AnimeSummaryCard_anime_previewThemes
|
||||
...AnimeSummaryCard_anime_expandable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ export async function getStaticProps({ params: { year } }) {
|
||||
|
||||
const { data, apiRequests } = await fetchData(gql`
|
||||
${AnimeSummaryCard.fragments.anime}
|
||||
${AnimeSummaryCard.fragments.previewThemes}
|
||||
${AnimeSummaryCard.fragments.expandable}
|
||||
|
||||
query($year: Int!) {
|
||||
@@ -63,7 +62,6 @@ export async function getStaticProps({ params: { year } }) {
|
||||
anime {
|
||||
slug
|
||||
...AnimeSummaryCard_anime
|
||||
...AnimeSummaryCard_anime_previewThemes
|
||||
...AnimeSummaryCard_anime_expandable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,9 @@ export const zoomIn = keyframes`
|
||||
|
||||
export const blurOut = keyframes`
|
||||
from {
|
||||
background-color: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
to {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
will-change: backdrop-filter;
|
||||
backdrop-filter: brightness(0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user