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:
Manuel S
2022-05-30 01:30:24 +02:00
committed by GitHub
parent 15c20d161e
commit 940c9b12a0
47 changed files with 721 additions and 486 deletions
+137 -131
View File
@@ -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
View File
@@ -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",
+1 -1
View File
@@ -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);
+5 -109
View File
@@ -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
}
}
}
`
+1 -1
View File
@@ -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>
+5 -2
View File
@@ -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 => (
+40 -22
View File
@@ -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);
+3 -3
View File
@@ -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>
);
}
});
+1
View File
@@ -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"]};
+24 -9
View File
@@ -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>
+31 -18
View File
@@ -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/>}
/>
);
}
+29 -16
View File
@@ -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}/>}
/>
);
}
+18 -14
View File
@@ -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" }}>
+1 -1
View File
@@ -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"
+29 -16
View File
@@ -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}`} />}
/>
);
}
+29 -16
View File
@@ -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}`} />}
/>
);
}
+31 -18
View File
@@ -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/>}
/>
);
}
+1
View File
@@ -27,6 +27,7 @@ Table.Row = styled.div`
`;
Table.Cell = styled.div`
grid-column: span var(--span, 1);
overflow: hidden;
`;
+99
View File
@@ -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
View File
@@ -1 +1,2 @@
export { Table } from "./Table";
export { ThemeTable } from "./ThemeTable";
+1 -1
View File
@@ -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>
+42
View File
@@ -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;
}
}
+22 -2
View File
@@ -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
}
`
};
+9
View File
@@ -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
}
`
};
+8 -9
View File
@@ -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 -1
View File
@@ -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";
+2 -2
View File
@@ -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&apos;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;
+15 -9
View File
@@ -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 = {
+14 -24
View File
@@ -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 {
+13 -2
View File
@@ -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;
}
+24 -10
View File
@@ -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
}),
}
};
+5 -11
View File
@@ -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
View File
@@ -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}
+2
View File
@@ -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
-2
View File
@@ -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
View File
@@ -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
}
};
}
+17 -8
View File
@@ -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>
+3 -1
View File
@@ -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}
</>
);
}
-2
View File
@@ -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
-2
View File
@@ -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
-2
View File
@@ -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
}
}
-2
View File
@@ -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
}
}
+2 -4
View File
@@ -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);
}
`;