feat: Migrated to Next.js (#87)
* Rewrote data layer to work on-demand. * Added several favicon formats. * Added image for SEO page previews. * Disabled GitHub pages as it no longer works. * Several other adjustments.
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
[
|
|
||||||
"babel-preset-gatsby",
|
|
||||||
{
|
|
||||||
"reactRuntime": "automatic"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"prismjs",
|
|
||||||
{
|
|
||||||
"languages": [
|
|
||||||
"jsx"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=http://localhost
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_API_URL=https://staging.animethemes.moe
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# NPM
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# Gatsby
|
|
||||||
/.cache
|
|
||||||
/public
|
|
||||||
|
|
||||||
# IDE (WebStorm)
|
|
||||||
*.iml
|
|
||||||
/.idea
|
|
||||||
@@ -1,46 +1,47 @@
|
|||||||
{
|
{
|
||||||
"globals": {
|
"extends": [
|
||||||
"__PATH_PREFIX__": true
|
"next/core-web-vitals",
|
||||||
},
|
"plugin:react/recommended"
|
||||||
"extends": [
|
],
|
||||||
"react-app",
|
"rules": {
|
||||||
"plugin:react/recommended"
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
],
|
],
|
||||||
"rules": {
|
"quotes": [
|
||||||
"quotes": [
|
"error",
|
||||||
"error",
|
"double",
|
||||||
"double",
|
{
|
||||||
{
|
"allowTemplateLiterals": true
|
||||||
"allowTemplateLiterals": true
|
}
|
||||||
}
|
],
|
||||||
],
|
"curly": [
|
||||||
"curly": [
|
"error",
|
||||||
"error",
|
"all"
|
||||||
"all"
|
],
|
||||||
],
|
"object-curly-spacing": [
|
||||||
"object-curly-spacing": [
|
"error",
|
||||||
"error",
|
"always"
|
||||||
"always"
|
],
|
||||||
],
|
"indent": [
|
||||||
"indent": [
|
"error",
|
||||||
"error",
|
4,
|
||||||
4,
|
{
|
||||||
{
|
"SwitchCase": 1
|
||||||
"SwitchCase": 1
|
}
|
||||||
}
|
],
|
||||||
],
|
"no-duplicate-imports": "error",
|
||||||
"no-duplicate-imports": "error",
|
"no-restricted-imports": [
|
||||||
"no-restricted-imports": [
|
"error",
|
||||||
"error",
|
{
|
||||||
{
|
"patterns": [
|
||||||
"patterns": [
|
"components/*/*"
|
||||||
"components/*/*"
|
]
|
||||||
]
|
}
|
||||||
}
|
],
|
||||||
],
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/self-closing-comp": "error",
|
||||||
"react/self-closing-comp": "error",
|
// For now we don't habe prop-types validation
|
||||||
// For now we don't habe prop-types validation
|
"react/prop-types": "off"
|
||||||
"react/prop-types": "off"
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
name: github pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
services:
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:latest
|
|
||||||
env:
|
|
||||||
MYSQL_ROOT_PASSWORD: test123
|
|
||||||
MYSQL_DATABASE: animethemes
|
|
||||||
ports:
|
|
||||||
- 3306
|
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
|
||||||
runs-on: ubuntu-18.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Import database dump
|
|
||||||
env:
|
|
||||||
PORT: ${{ job.services.mariadb.ports[3306] }}
|
|
||||||
run: |
|
|
||||||
sudo /etc/init.d/mysql start
|
|
||||||
curl -L https://github.com/AnimeThemes/animethemes-db-dump/raw/main/mysql/animethemes-db-dump-create-tables.sql | mysql --user=root --password=test123 --host=127.0.0.1 --port="$PORT" animethemes
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v2.1.2
|
|
||||||
with:
|
|
||||||
node-version: '12.x'
|
|
||||||
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
|
||||||
|
|
||||||
- name: Build Gatsby pages
|
|
||||||
run: npm run build-with-paths
|
|
||||||
env:
|
|
||||||
GATSBY_PATH_PREFIX: /animethemes-web
|
|
||||||
GATSBY_CI: true
|
|
||||||
SOURCE_PLUGIN: DB
|
|
||||||
DB_USER: root
|
|
||||||
DB_PASSWORD: test123
|
|
||||||
DB_PORT: ${{ job.services.mariadb.ports[3306] }}
|
|
||||||
|
|
||||||
- name: Deploy
|
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./public
|
|
||||||
@@ -1,14 +1,37 @@
|
|||||||
# General
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
# NPM
|
# dependencies
|
||||||
node_modules
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
# Gatsby
|
# testing
|
||||||
/.cache
|
/coverage
|
||||||
/public
|
|
||||||
/schema.json
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
# IDE (WebStorm)
|
# IDE (WebStorm)
|
||||||
*.iml
|
*.iml
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaPath": "schema.json"
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017 paranarimasu
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,41 +1,58 @@
|
|||||||
# AnimeThemes Web Client
|
# AnimeThemes Web Client
|
||||||
|
|
||||||
|
1. [Configuration](#configuration)
|
||||||
|
2. [Development](#development)
|
||||||
|
3. [Deployment](#deployment)
|
||||||
|
4. [Used technologies](#used-technologies)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To get started you need to define some environment variables. This can be done by creating a `.env.local` file in the
|
||||||
|
root directory.
|
||||||
|
|
||||||
|
This is a list of all available options:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
; Datebase configuration
|
||||||
|
; Only username and password are required.
|
||||||
|
; All other fields take the specified values by default, but can be changed if wanted.
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=...
|
||||||
|
DB_PASSWORD=...
|
||||||
|
DB_DATABASE=animethemes
|
||||||
|
|
||||||
|
; (Optional) The URL to the AnimeThemes API to use.
|
||||||
|
; For development this is "http://localhost" by default.
|
||||||
|
; For production this is "https://staging.animethemes.moe" by default.
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost
|
||||||
|
|
||||||
|
; (Optional) The URL from which video files should be served.
|
||||||
|
; By default this is "https://animethemes.moe".
|
||||||
|
NEXT_PUBLIC_VIDEO_URL=https://animethemes.moe
|
||||||
|
|
||||||
|
; (Optional) The base path the app should be hosted on.
|
||||||
|
; By default the app is hosted on the root path (/).
|
||||||
|
NEXT_PUBLIC_BASE_PATH=/wiki
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on environment variables see the [Next.js documentation](https://nextjs.org/docs/basic-features/environment-variables).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
1. `npm install`
|
1. `npm install`
|
||||||
2. `npm run develop`
|
2. `npm run dev`
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Hosting on root path
|
1. `npm install` (If not done already.)
|
||||||
|
2. `npm run build` generates all static pages and produces a production ready bundle.
|
||||||
1. `npm run build`
|
3. `npm run start` starts the server.
|
||||||
2. Upload the contents of `public` to your server.
|
4. The server is now listening on port 3000 for incoming requests.
|
||||||
|
|
||||||
### Hosting on sub-path
|
|
||||||
|
|
||||||
1. Adjust `pathPrefix` in `gatsby-config.js` to your server environment.
|
|
||||||
2. `npm run build-with-paths`
|
|
||||||
3. Upload the contents of `public` to your server.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
You can configure certain settings by creating a `.env.development` (for development) or `.env.production` (for deployment)
|
|
||||||
in the client root directory and include the following:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
; The URL to the AnimeThemes API to use.
|
|
||||||
; If not specified, "https://staging.animethemes.moe" is used as a default.
|
|
||||||
GATSBY_API_URL = http://localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
### Apache Configuration
|
|
||||||
|
|
||||||
`.htaccess` files can be placed in `static` and will be copied to the `public` folder during build.
|
|
||||||
|
|
||||||
## Used technologies
|
## Used technologies
|
||||||
|
|
||||||
- [Gatsby](https://www.gatsbyjs.com/)
|
- [Next.js](https://www.nextjs.org/)
|
||||||
- [styled-components](https://styled-components.com/)
|
- [styled-components](https://styled-components.com/)
|
||||||
- [react-query](https://react-query.tanstack.com/)
|
- [react-query](https://react-query.tanstack.com/)
|
||||||
- [Font Awesome](https://fontawesome.com/)
|
- [Font Awesome](https://fontawesome.com/)
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export function shouldUpdateScroll({
|
|
||||||
routerProps,
|
|
||||||
prevRouterProps
|
|
||||||
}) {
|
|
||||||
return routerProps?.location?.pathname !== prevRouterProps?.location?.pathname;
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import siteConfig from "./site-config";
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
|
|
||||||
// Load environment configuration
|
|
||||||
dotenv.config({
|
|
||||||
path: `.env.${process.env.NODE_ENV}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map every path in "src" to its directory name so we can import files like this:
|
|
||||||
//
|
|
||||||
// import Button from "components/button";
|
|
||||||
//
|
|
||||||
// Otherwise we would have to provide relative paths which makes refactoring harder.
|
|
||||||
const aliases = fs.readdirSync(path.join(__dirname, "src")).reduce((obj, directory) => {
|
|
||||||
obj[directory] = path.join(__dirname, "src", directory);
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
// If Gatsby already set up a path prefix, use that instead
|
|
||||||
const pathPrefix = process.env.GATSBY_PATH_PREFIX || siteConfig.pathPrefix;
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
pathPrefix: pathPrefix,
|
|
||||||
siteMetadata: {
|
|
||||||
lang: siteConfig.siteLanguage,
|
|
||||||
siteName: siteConfig.siteName,
|
|
||||||
description: siteConfig.description,
|
|
||||||
titleTemplate: `%s · ${siteConfig.siteName}`,
|
|
||||||
siteUrl: path.posix.join((process.env.SITE_URL || siteConfig.rootUrl), pathPrefix)
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
process.env.SOURCE_PLUGIN === "DB"
|
|
||||||
? "gatsby-source-animethemes-db"
|
|
||||||
: "gatsby-source-animethemes",
|
|
||||||
"gatsby-source-animebracket",
|
|
||||||
"gatsby-plugin-styled-components",
|
|
||||||
"gatsby-plugin-react-helmet",
|
|
||||||
{
|
|
||||||
resolve: "gatsby-plugin-root-import",
|
|
||||||
options: aliases
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resolve: "gatsby-plugin-layout",
|
|
||||||
options: {
|
|
||||||
component: require.resolve("./src/layout/index.js"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gatsby-plugin-fontawesome-css",
|
|
||||||
{
|
|
||||||
resolve: "gatsby-plugin-manifest",
|
|
||||||
options: {
|
|
||||||
icon: "src/images/icon.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gatsby-plugin-eslint",
|
|
||||||
"gatsby-plugin-sass"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
config.plugins.push("gatsby-plugin-extract-schema");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// eslint-disable-next-line no-native-reassign
|
|
||||||
require = require("esm")(module);
|
|
||||||
module.exports = require("./gatsby-config.esm.js");
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import createVideoSlug from "./src/utils/createVideoSlug";
|
|
||||||
|
|
||||||
const seasonOrder = [ "Winter", "Spring", "Summer", "Fall" ];
|
|
||||||
|
|
||||||
export async function createPages({ actions: { createPage }, graphql }) {
|
|
||||||
const { data } = await graphql(`
|
|
||||||
query {
|
|
||||||
allAnime {
|
|
||||||
nodes {
|
|
||||||
slug
|
|
||||||
themes {
|
|
||||||
# We can't use fragments in gatsby-node.js,
|
|
||||||
# so this is mostly a copy of the fragment found in
|
|
||||||
# createVideoSlug.fragment.js.
|
|
||||||
# (https://stackoverflow.com/a/49351248)
|
|
||||||
slug
|
|
||||||
entries {
|
|
||||||
id
|
|
||||||
version
|
|
||||||
videos {
|
|
||||||
id
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groupedByYear: group(field: year) {
|
|
||||||
year: fieldValue
|
|
||||||
nodes {
|
|
||||||
season
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allSeries {
|
|
||||||
nodes {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allStudio {
|
|
||||||
nodes {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allArtist {
|
|
||||||
nodes {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allBracket {
|
|
||||||
nodes {
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
|
||||||
for (const { slug, themes } of data.allAnime.nodes) {
|
|
||||||
createPage({
|
|
||||||
path: `/anime/${slug}`,
|
|
||||||
component: require.resolve("./src/templates/anime.js"),
|
|
||||||
context: { slug }
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const theme of themes) {
|
|
||||||
for (const entry of theme.entries) {
|
|
||||||
for (const video of entry.videos) {
|
|
||||||
const videoSlug = createVideoSlug(theme, entry, video);
|
|
||||||
|
|
||||||
createPage({
|
|
||||||
path: `/anime/${slug}/${videoSlug}`,
|
|
||||||
component: require.resolve("./src/templates/video.js"),
|
|
||||||
context: { id: video.id, entryId: entry.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { year, nodes } of data.allAnime.groupedByYear) {
|
|
||||||
const yearNumeric = +year;
|
|
||||||
|
|
||||||
// Get unique seasons
|
|
||||||
const seasonList = nodes
|
|
||||||
.map((node) => node.season)
|
|
||||||
.filter((value, index, self) => self.indexOf(value) === index)
|
|
||||||
.sort((a, b) => seasonOrder.indexOf(a) - seasonOrder.indexOf(b));
|
|
||||||
|
|
||||||
createPage({
|
|
||||||
path: `/year/${year}`,
|
|
||||||
component: require.resolve("./src/templates/year.js"),
|
|
||||||
context: { year: yearNumeric, seasonList }
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const season of seasonList) {
|
|
||||||
createPage({
|
|
||||||
path: `/year/${year}/${season.toLowerCase()}`,
|
|
||||||
component: require.resolve("./src/templates/season.js"),
|
|
||||||
context: { year: yearNumeric, season, seasonList }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { slug } of data.allSeries.nodes) {
|
|
||||||
createPage({
|
|
||||||
path: `/series/${slug}`,
|
|
||||||
component: require.resolve("./src/templates/series.js"),
|
|
||||||
context: { slug }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { slug } of data.allStudio.nodes) {
|
|
||||||
createPage({
|
|
||||||
path: `/studio/${slug}`,
|
|
||||||
component: require.resolve("./src/templates/studio.js"),
|
|
||||||
context: { slug }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { slug } of data.allArtist.nodes) {
|
|
||||||
createPage({
|
|
||||||
path: `/artist/${slug}`,
|
|
||||||
component: require.resolve("./src/templates/artist.js"),
|
|
||||||
context: { slug }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const { slug } of data.allBracket.nodes) {
|
|
||||||
createPage({
|
|
||||||
path: `/bracket/${slug}`,
|
|
||||||
component: require.resolve("./src/templates/bracket.js"),
|
|
||||||
context: { slug }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entity of [ null, "anime", "theme", "artist", "series", "studio" ]) {
|
|
||||||
createPage({
|
|
||||||
path: `/search${entity ? `/${entity}` : ``}`,
|
|
||||||
component: require.resolve("./src/templates/search.js"),
|
|
||||||
context: { entity }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// eslint-disable-next-line no-native-reassign
|
|
||||||
require = require("esm")(module);
|
|
||||||
module.exports = require("./gatsby-node.esm.js");
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const ThemeInjection = () => {
|
|
||||||
// language=JavaScript
|
|
||||||
const injectTheme = `
|
|
||||||
(function() {
|
|
||||||
const body = document.body;
|
|
||||||
const theme = window.localStorage.getItem("theme");
|
|
||||||
|
|
||||||
if (theme) {
|
|
||||||
body.setAttribute("theme", theme);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<script
|
|
||||||
dangerouslySetInnerHTML={{ __html: injectTheme }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function onRenderBody({ setBodyAttributes, setPreBodyComponents }) {
|
|
||||||
setBodyAttributes({ theme: "dark" });
|
|
||||||
setPreBodyComponents(<ThemeInjection />);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||||
|
enabled: process.env.ANALYZE === "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = withBundleAnalyzer({
|
||||||
|
basePath: process.env.NEXT_PUBLIC_BASE_PATH || "",
|
||||||
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
styledComponents: true
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,57 +1,41 @@
|
|||||||
{
|
{
|
||||||
"name": "anime-themes",
|
"name": "animethemes-web",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"scripts": {
|
"private": true,
|
||||||
"develop": "gatsby develop",
|
"scripts": {
|
||||||
"build": "gatsby build",
|
"dev": "next dev",
|
||||||
"build-with-paths": "gatsby build --prefix-paths"
|
"build": "next build",
|
||||||
},
|
"start": "next start",
|
||||||
"dependencies": {
|
"lint": "next lint"
|
||||||
"@fontsource/nunito": "^4.5.0",
|
},
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
"dependencies": {
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
"@reach/listbox": "^0.16.1",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@react-hook/resize-observer": "^1.2.2",
|
"@graphql-tools/merge": "^8.2.1",
|
||||||
"babel-plugin-prismjs": "^2.0.1",
|
"@graphql-tools/schema": "^8.2.0",
|
||||||
"babel-plugin-styled-components": "^1.11.1",
|
"@next/bundle-analyzer": "^12.0.3",
|
||||||
"babel-preset-gatsby": "^1.3.0",
|
"@reach/listbox": "^0.16.1",
|
||||||
"common-tags": "^1.8.0",
|
"common-tags": "^1.8.0",
|
||||||
"dotenv": "^8.2.0",
|
"framer-motion": "^4.1.17",
|
||||||
"esm": "^3.2.25",
|
"graphql": "^15.6.1",
|
||||||
"framer-motion": "^4.1.3",
|
"knex": "^0.95.11",
|
||||||
"gatsby": "^3.13.0",
|
"lodash": "^4.17.21",
|
||||||
"gatsby-plugin-fontawesome-css": "^1.1.0",
|
"mysql2": "^2.3.3",
|
||||||
"gatsby-plugin-layout": "^2.3.0",
|
"next": "^12.0.5-canary.6",
|
||||||
"gatsby-plugin-manifest": "^3.3.0",
|
"prismjs": "^1.25.0",
|
||||||
"gatsby-plugin-react-helmet": "^4.3.0",
|
"react": "^17.0.2",
|
||||||
"gatsby-plugin-root-import": "^2.0.6",
|
"react-dom": "^17.0.2",
|
||||||
"gatsby-plugin-sass": "^4.3.0",
|
"react-query": "^3.29.0",
|
||||||
"gatsby-plugin-styled-components": "^4.3.0",
|
"sass": "^1.43.4",
|
||||||
"gatsby-source-animethemes": "file:plugins/gatsby-source-animethemes",
|
"styled-components": "^5.3.3",
|
||||||
"gatsby-source-animethemes-db": "file:plugins/gatsby-source-animethemes-db",
|
"styled-system": "^5.1.5",
|
||||||
"luxon": "^2.0.2",
|
"use-debounce": "^7.0.0",
|
||||||
"node-fetch": "^2.6.1",
|
"use-media": "^1.4.0"
|
||||||
"prismjs": "^1.24.1",
|
},
|
||||||
"react": "^17.0.1",
|
"devDependencies": {
|
||||||
"react-dom": "^17.0.1",
|
"eslint": "7.32.0",
|
||||||
"react-helmet": "^6.1.0",
|
"eslint-config-next": "^12.0.3"
|
||||||
"react-query": "^3.18.1",
|
}
|
||||||
"sass": "^1.32.8",
|
|
||||||
"styled-components": "^5.2.0",
|
|
||||||
"styled-system": "^5.1.5",
|
|
||||||
"use-debounce": "^6.0.1",
|
|
||||||
"use-media": "^1.4.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@hot-loader/react-dom": "^17.0.1",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"eslint": "^7.21.0",
|
|
||||||
"eslint-config-react-app": "^6.0.0",
|
|
||||||
"eslint-loader": "^4.0.2",
|
|
||||||
"eslint-plugin-react": "^7.22.0",
|
|
||||||
"gatsby-plugin-eslint": "^3.0.0",
|
|
||||||
"gatsby-plugin-extract-schema": "^0.1.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
const fetch = require("node-fetch");
|
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
const mappings = JSON.parse(fs.readFileSync("./plugins/gatsby-source-animebracket/mappings.json", "utf8"));
|
|
||||||
|
|
||||||
exports.onPreInit = ({ reporter }) => reporter.info("Loaded gatsby-source-animebracket");
|
|
||||||
|
|
||||||
exports.createSchemaCustomization = ({ actions }) => {
|
|
||||||
const { createTypes } = actions;
|
|
||||||
createTypes(`
|
|
||||||
type Bracket implements Node {
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
currentRound: BracketRound @link(by: "id")
|
|
||||||
currentGroup: Int
|
|
||||||
rounds: [BracketRound] @link(by: "bracket.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type BracketRound implements Node {
|
|
||||||
bracket: Bracket @link(by: "id")
|
|
||||||
tier: Int!
|
|
||||||
name: String
|
|
||||||
pairings: [BracketPairing] @link(by: "round.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type BracketPairing implements Node {
|
|
||||||
round: BracketRound @link(by: "id")
|
|
||||||
order: Int!
|
|
||||||
group: Int!
|
|
||||||
characterA: BracketCharacter @link(by: "id")
|
|
||||||
characterB: BracketCharacter @link(by: "id")
|
|
||||||
votesA: Int
|
|
||||||
votesB: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type BracketCharacter implements Node {
|
|
||||||
pairing: BracketPairing @link(by: "id")
|
|
||||||
idRaw: Int!
|
|
||||||
seed: Int!
|
|
||||||
name: String!
|
|
||||||
source: String!
|
|
||||||
theme: Theme @link(by: "idRaw")
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
|
|
||||||
const helpers = {
|
|
||||||
...actions,
|
|
||||||
createNodeId,
|
|
||||||
createContentDigest
|
|
||||||
};
|
|
||||||
|
|
||||||
const perma = "best-anime-opening-ix-salty-arrow";
|
|
||||||
const bracketResponse = await fetch(`https://animebracket.com/api/bracket/${perma}`);
|
|
||||||
const bracket = await bracketResponse.json();
|
|
||||||
const roundsResponse = await fetch(`https://animebracket.com/api/results/${perma}`)
|
|
||||||
const rounds = await roundsResponse.json();
|
|
||||||
const currentRoundResponse = await fetch(`https://animebracket.com/api/rounds/${perma}`);
|
|
||||||
const currentRound = await currentRoundResponse.json();
|
|
||||||
|
|
||||||
const bracketNode = createNodeFromData({
|
|
||||||
id: bracket.id,
|
|
||||||
slug: perma,
|
|
||||||
name: bracket.name,
|
|
||||||
currentRound: currentRound ? createNodeId(`BracketRound-${bracket.id}-${currentRound[0].tier}`) : null,
|
|
||||||
currentGroup: currentRound ? currentRound[0].group : null,
|
|
||||||
rounds: rounds.map((round) => createNodeId(`BracketRound-${bracket.id}-${round[0].tier}`))
|
|
||||||
}, "Bracket", helpers);
|
|
||||||
|
|
||||||
for (const round of rounds) {
|
|
||||||
sourceRound(round);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentRound) {
|
|
||||||
sourceRound(currentRound);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceRound(round) {
|
|
||||||
const tier = +round[0].tier;
|
|
||||||
|
|
||||||
const roundNode = createNodeFromData({
|
|
||||||
id: `${bracket.id}-${tier}`,
|
|
||||||
bracket: bracketNode.id,
|
|
||||||
tier: tier,
|
|
||||||
pairings: round.map((pairing) => createNodeId(`BracketPairing-${pairing.id}`))
|
|
||||||
}, "BracketRound", helpers);
|
|
||||||
|
|
||||||
for (const pairing of round) {
|
|
||||||
if (pairing.filler) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pairingNode = createNodeFromData({
|
|
||||||
id: pairing.id,
|
|
||||||
round: roundNode.id,
|
|
||||||
order: +pairing.order,
|
|
||||||
group: +pairing.group,
|
|
||||||
characterA: createNodeId(`BracketCharacter-${pairing.character1.id}`),
|
|
||||||
characterB: createNodeId(`BracketCharacter-${pairing.character2.id}`),
|
|
||||||
votesA: pairing.character1.votes || null,
|
|
||||||
votesB: pairing.character2.votes || null
|
|
||||||
}, "BracketPairing", helpers);
|
|
||||||
|
|
||||||
for (const character of [ pairing.character1, pairing.character2 ]) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: character.id,
|
|
||||||
idRaw: character.id,
|
|
||||||
pairing: pairingNode.id,
|
|
||||||
seed: character.seed,
|
|
||||||
name: character.name,
|
|
||||||
source: character.source,
|
|
||||||
theme: mappings[character.id]
|
|
||||||
}, "BracketCharacter", helpers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createResolvers = ({ createResolvers }) => {
|
|
||||||
createResolvers({
|
|
||||||
BracketRound: {
|
|
||||||
name: {
|
|
||||||
resolve(source) {
|
|
||||||
switch (source.pairings.length) {
|
|
||||||
case 1:
|
|
||||||
return "Finals";
|
|
||||||
case 2:
|
|
||||||
return "Semifinals";
|
|
||||||
case 4:
|
|
||||||
return "Quarterfinals";
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function createNodeFromData(item, nodeType, helpers) {
|
|
||||||
const node = {
|
|
||||||
...item,
|
|
||||||
id: helpers.createNodeId(`${nodeType}-${item.id}`),
|
|
||||||
parent: null,
|
|
||||||
children: [],
|
|
||||||
internal: {
|
|
||||||
type: nodeType,
|
|
||||||
content: JSON.stringify(item),
|
|
||||||
contentDigest: helpers.createContentDigest(item)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
helpers.createNode(node)
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gatsby-source-animebracket",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
exports.onPreInit = ({ reporter }) => reporter.info("Loaded gatsby-source-animethemes-db");
|
|
||||||
|
|
||||||
exports.createSchemaCustomization = require("./src/create-schema-customization");
|
|
||||||
|
|
||||||
exports.sourceNodes = require("./src/source-nodes");
|
|
||||||
|
|
||||||
exports.createResolvers = require("./src/create-resolvers");
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gatsby-source-animethemes-db",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"requires": true,
|
|
||||||
"dependencies": {
|
|
||||||
"denque": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
|
|
||||||
},
|
|
||||||
"generate-function": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
|
||||||
"requires": {
|
|
||||||
"is-property": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"iconv-lite": {
|
|
||||||
"version": "0.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
|
|
||||||
"integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
|
|
||||||
"requires": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-property": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ="
|
|
||||||
},
|
|
||||||
"long": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
|
||||||
},
|
|
||||||
"lru-cache": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
|
||||||
"requires": {
|
|
||||||
"yallist": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mysql2": {
|
|
||||||
"version": "2.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz",
|
|
||||||
"integrity": "sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==",
|
|
||||||
"requires": {
|
|
||||||
"denque": "^1.4.1",
|
|
||||||
"generate-function": "^2.3.1",
|
|
||||||
"iconv-lite": "^0.6.2",
|
|
||||||
"long": "^4.0.0",
|
|
||||||
"lru-cache": "^6.0.0",
|
|
||||||
"named-placeholders": "^1.1.2",
|
|
||||||
"seq-queue": "^0.0.5",
|
|
||||||
"sqlstring": "^2.3.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"sqlstring": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"named-placeholders": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==",
|
|
||||||
"requires": {
|
|
||||||
"lru-cache": "^4.1.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lru-cache": {
|
|
||||||
"version": "4.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
|
|
||||||
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
|
|
||||||
"requires": {
|
|
||||||
"pseudomap": "^1.0.2",
|
|
||||||
"yallist": "^2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"yallist": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
|
||||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pseudomap": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
|
||||||
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
|
|
||||||
},
|
|
||||||
"safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
|
||||||
},
|
|
||||||
"seq-queue": {
|
|
||||||
"version": "0.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
|
||||||
"integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4="
|
|
||||||
},
|
|
||||||
"yallist": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gatsby-source-animethemes-db",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"dependencies": {
|
|
||||||
"mysql2": "^2.2.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
const { animeSeason, videoSource, imageFacet, resourceSite, videoOverlap, themeType } = require("./enums");
|
|
||||||
|
|
||||||
module.exports = ({ createResolvers }) => {
|
|
||||||
createResolvers({
|
|
||||||
Anime: {
|
|
||||||
...enumResolver({
|
|
||||||
season: animeSeason
|
|
||||||
}),
|
|
||||||
...pivotResolver({
|
|
||||||
series: { type: "AnimeSeries", from: "anime", to: "series" },
|
|
||||||
resources: { type: "AnimeResource", from: "anime", to: "resource" },
|
|
||||||
images: { type: "AnimeImage", from: "anime", to: "image" },
|
|
||||||
studios: { type: "AnimeStudio", from: "anime", to: "studio" }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Artist: {
|
|
||||||
...pivotResolver({
|
|
||||||
resources: { type: "ArtistResource", from: "artist", to: "resource" },
|
|
||||||
images: { type: "ArtistImage", from: "artist", to: "image" }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Entry: {
|
|
||||||
...pivotResolver({
|
|
||||||
videos: { type: "EntryVideo", from: "entry", to: "video" }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Image: {
|
|
||||||
...enumResolver({
|
|
||||||
facet: imageFacet
|
|
||||||
}),
|
|
||||||
link: {
|
|
||||||
resolve(source) {
|
|
||||||
return `https://staging.animethemes.moe/image/${source.idRaw}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Resource: {
|
|
||||||
...enumResolver({
|
|
||||||
site: resourceSite
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Series: {
|
|
||||||
...pivotResolver({
|
|
||||||
anime: { type: "AnimeSeries", from: "series", to: "anime" }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Studio: {
|
|
||||||
...pivotResolver({
|
|
||||||
anime: { type: "AnimeStudio", from: "studio", to: "anime" },
|
|
||||||
resources: { type: "StudioResource", from: "studio", to: "resource" }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Theme: {
|
|
||||||
...enumResolver({
|
|
||||||
type: themeType
|
|
||||||
})
|
|
||||||
},
|
|
||||||
Video: {
|
|
||||||
...enumResolver({
|
|
||||||
overlap: videoOverlap,
|
|
||||||
source: videoSource
|
|
||||||
}),
|
|
||||||
...pivotResolver({
|
|
||||||
entries: { type: "EntryVideo", from: "video", to: "entry" }
|
|
||||||
}),
|
|
||||||
tags: {
|
|
||||||
resolve(source) {
|
|
||||||
const tags = [];
|
|
||||||
|
|
||||||
if (source.nc) {
|
|
||||||
tags.push("NC");
|
|
||||||
}
|
|
||||||
if (source.source === 2 || source.source === 3) {
|
|
||||||
tags.push(videoSource.get(source.source));
|
|
||||||
}
|
|
||||||
if (source.resolution && source.resolution !== 720) {
|
|
||||||
tags.push(source.resolution);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source.subbed) {
|
|
||||||
tags.push("Subbed");
|
|
||||||
} else if (source.lyrics) {
|
|
||||||
tags.push("Lyrics");
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags.join("");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
link: {
|
|
||||||
resolve(source) {
|
|
||||||
return `https://v.animethemes.moe/${source.basename}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function enumResolver(propertyToEnumMap) {
|
|
||||||
return Object.fromEntries(Object.entries(propertyToEnumMap).map(([ property, enumMap ]) => {
|
|
||||||
return [
|
|
||||||
property,
|
|
||||||
{
|
|
||||||
resolve(source) {
|
|
||||||
return enumMap.get(source[property]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function pivotResolver(propertyToPivotConfig) {
|
|
||||||
return Object.fromEntries(Object.entries(propertyToPivotConfig).map(([ property, pivotConfig ]) => {
|
|
||||||
return [
|
|
||||||
property,
|
|
||||||
{
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getAllNodes({ type: pivotConfig.type })
|
|
||||||
.filter((node) => node[pivotConfig.from] === source.id)
|
|
||||||
.map((node) => context.nodeModel.getNodeById({ id: node[pivotConfig.to] }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
module.exports = ({ actions }) => {
|
|
||||||
const { createTypes } = actions;
|
|
||||||
createTypes(`
|
|
||||||
interface Entity {
|
|
||||||
idRaw: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
type Anime implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
name: String!
|
|
||||||
slug: String!
|
|
||||||
year: Int!
|
|
||||||
season: String
|
|
||||||
synopsis: String
|
|
||||||
synonyms: [Synonym] @link(by: "anime.id", from: "id")
|
|
||||||
themes: [Theme] @link(by: "anime.id", from: "id")
|
|
||||||
series: [Series]
|
|
||||||
resources: [Resource]
|
|
||||||
images: [Image]
|
|
||||||
studios: [Studio]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Synonym implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
text: String!
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Theme implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
slug: String!
|
|
||||||
type: String!
|
|
||||||
sequence: Int!
|
|
||||||
group: String
|
|
||||||
song: Song! @link(by: "id")
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
entries: [Entry] @link(by: "theme.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
version: Int
|
|
||||||
episodes: String
|
|
||||||
nsfw: Boolean!
|
|
||||||
spoiler: Boolean!
|
|
||||||
theme: Theme! @link(by: "id")
|
|
||||||
videos: [Video]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Video implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
filename: String!
|
|
||||||
basename: String!
|
|
||||||
link: String
|
|
||||||
resolution: Int
|
|
||||||
nc: Boolean!
|
|
||||||
subbed: Boolean!
|
|
||||||
lyrics: Boolean!
|
|
||||||
uncen: Boolean!
|
|
||||||
source: String
|
|
||||||
overlap: String
|
|
||||||
tags: String
|
|
||||||
entries: [Entry]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Song implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
title: String!
|
|
||||||
themes: [Theme] @link(by: "song.id", from: "id")
|
|
||||||
performances: [Performance] @link(by: "song.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Performance implements Node {
|
|
||||||
song: Song! @link(by: "id")
|
|
||||||
artist: Artist! @link(by: "id")
|
|
||||||
as: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Artist implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
performances: [Performance] @link(by: "artist.id", from: "id")
|
|
||||||
members: [ArtistMembership] @link(by: "group.id", from: "id")
|
|
||||||
groups: [ArtistMembership] @link(by: "member.id", from: "id")
|
|
||||||
resources: [Resource]
|
|
||||||
images: [Image]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtistMembership implements Node {
|
|
||||||
group: Artist! @link(by: "id")
|
|
||||||
member: Artist! @link(by: "id")
|
|
||||||
as: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Series implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
anime: [Anime]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resource implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
link: String!
|
|
||||||
site: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Image implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
facet: String!
|
|
||||||
link: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Studio implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
anime: [Anime]
|
|
||||||
resources: [Resource]
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnimeSeries implements Node {
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
series: Series! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntryVideo implements Node {
|
|
||||||
entry: Entry! @link(by: "id")
|
|
||||||
video: Video! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnimeResource implements Node {
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
resource: Resource! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtistResource implements Node {
|
|
||||||
artist: Artist! @link(by: "id")
|
|
||||||
resource: Resource! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnimeImage implements Node {
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
image: Image! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type ArtistImage implements Node {
|
|
||||||
artist: Artist! @link(by: "id")
|
|
||||||
image: Image! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnimeStudio implements Node {
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
studio: Studio! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type StudioResource implements Node {
|
|
||||||
studio: Studio! @link(by: "id")
|
|
||||||
resource: Resource! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Announcement implements Node & Entity {
|
|
||||||
idRaw: Int
|
|
||||||
content: String!
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
@@ -1,330 +0,0 @@
|
|||||||
const mysql = require("mysql2");
|
|
||||||
|
|
||||||
module.exports = async ({ actions, createNodeId, createContentDigest, reporter }) => {
|
|
||||||
const helpers = {
|
|
||||||
...actions,
|
|
||||||
createNodeId,
|
|
||||||
createContentDigest
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Implement incremental data fetching when pivots have timestamps
|
|
||||||
// const lastFetched = await cache.get("last-fetched");
|
|
||||||
|
|
||||||
// getNodes().filter(isPluginNode).forEach((node) => helpers.touchNode(node));
|
|
||||||
|
|
||||||
// const now = new Date();
|
|
||||||
|
|
||||||
const connection = mysql.createConnection({
|
|
||||||
host: process.env.DB_HOST || "127.0.0.1",
|
|
||||||
port: process.env.DB_PORT || 3306,
|
|
||||||
user: process.env.DB_USER,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_DATABASE || "animethemes"
|
|
||||||
});
|
|
||||||
|
|
||||||
async function query(sql) {
|
|
||||||
reporter.info(`Executing query: ${sql}`);
|
|
||||||
return new Promise((resolve, reject) => connection.query(sql, (error, results) => {
|
|
||||||
if (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
reporter.info(`Got ${results.length} results`);
|
|
||||||
resolve(results);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectAllFrom(table, isPivot = false, tableToA, columnFromA, columnToA, tableToB, columnFromB, columnToB) {
|
|
||||||
let sql = `SELECT ${table}.* FROM ${table}`;
|
|
||||||
|
|
||||||
if (!isPivot) {
|
|
||||||
sql += " WHERE deleted_at IS NULL";
|
|
||||||
} else {
|
|
||||||
sql += ` INNER JOIN ${tableToA} AS a ON (${table}.${columnFromA} = a.${columnToA})
|
|
||||||
INNER JOIN ${tableToB} AS b ON (${table}.${columnFromB} = b.${columnToB})
|
|
||||||
WHERE a.deleted_at IS NULL AND b.deleted_at IS NULL`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const anime of await selectAllFrom("anime")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: anime.anime_id,
|
|
||||||
idRaw: anime.anime_id,
|
|
||||||
name: anime.name,
|
|
||||||
slug: anime.slug,
|
|
||||||
year: anime.year,
|
|
||||||
season: anime.season,
|
|
||||||
synopsis: anime.synopsis,
|
|
||||||
resources: [],
|
|
||||||
images: []
|
|
||||||
}, "Anime", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const synonym of await selectAllFrom("anime_synonyms")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: synonym.synonym_id,
|
|
||||||
idRaw: synonym.synonym_id,
|
|
||||||
text: synonym.text,
|
|
||||||
anime: createNodeId(`Anime-${synonym.anime_id}`)
|
|
||||||
}, "Synonym", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const theme of await selectAllFrom("anime_themes")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: theme.theme_id,
|
|
||||||
idRaw: theme.theme_id,
|
|
||||||
slug: theme.slug,
|
|
||||||
type: theme.type,
|
|
||||||
sequence: theme.sequence || 0,
|
|
||||||
group: theme.group,
|
|
||||||
song: createNodeId(`Song-${theme.song_id}`),
|
|
||||||
anime: createNodeId(`Anime-${theme.anime_id}`)
|
|
||||||
}, "Theme", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of await selectAllFrom("anime_theme_entries")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: entry.entry_id,
|
|
||||||
idRaw: entry.entry_id,
|
|
||||||
version: entry.version || 1,
|
|
||||||
episodes: entry.episodes,
|
|
||||||
nsfw: entry.nsfw,
|
|
||||||
spoiler: entry.spoiler,
|
|
||||||
theme: createNodeId(`Theme-${entry.theme_id}`),
|
|
||||||
videos: []
|
|
||||||
}, "Entry", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const video of await selectAllFrom("videos")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: video.video_id,
|
|
||||||
idRaw: video.video_id,
|
|
||||||
filename: video.filename,
|
|
||||||
basename: video.basename,
|
|
||||||
link: video.link,
|
|
||||||
resolution: video.resolution,
|
|
||||||
nc: video.nc,
|
|
||||||
subbed: video.subbed,
|
|
||||||
lyrics: video.lyrics,
|
|
||||||
uncen: video.uncen,
|
|
||||||
source: video.source,
|
|
||||||
overlap: video.overlap,
|
|
||||||
entries: []
|
|
||||||
}, "Video", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const song of await selectAllFrom("songs")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: song.song_id,
|
|
||||||
idRaw: song.song_id,
|
|
||||||
title: song.title
|
|
||||||
}, "Song", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artist of await selectAllFrom("artists")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: artist.artist_id,
|
|
||||||
idRaw: artist.artist_id,
|
|
||||||
slug: artist.slug,
|
|
||||||
name: artist.name,
|
|
||||||
resources: [],
|
|
||||||
images: []
|
|
||||||
}, "Artist", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const series of await selectAllFrom("series")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: series.series_id,
|
|
||||||
idRaw: series.series_id,
|
|
||||||
slug: series.slug,
|
|
||||||
name: series.name
|
|
||||||
}, "Series", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const resource of await selectAllFrom("resources")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: resource.resource_id,
|
|
||||||
idRaw: resource.resource_id,
|
|
||||||
link: resource.link,
|
|
||||||
site: resource.site
|
|
||||||
}, "Resource", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const image of await selectAllFrom("images")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: image.image_id,
|
|
||||||
idRaw: image.image_id,
|
|
||||||
facet: image.facet
|
|
||||||
}, "Image", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const studio of await selectAllFrom("studios")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: studio.studio_id,
|
|
||||||
idRaw: studio.studio_id,
|
|
||||||
slug: studio.slug,
|
|
||||||
name: studio.name
|
|
||||||
}, "Studio", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const announcement of await selectAllFrom("announcements")) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: announcement.announcement_id,
|
|
||||||
idRaw: announcement.announcement_id,
|
|
||||||
content: announcement.content
|
|
||||||
}, "Announcement", helpers);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
reporter.warn("Error while getting announcements. This should only happen on GitHub pages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pivot types
|
|
||||||
|
|
||||||
for (const performance of await selectAllFrom(
|
|
||||||
"artist_song", true,
|
|
||||||
"artists", "artist_id", "artist_id",
|
|
||||||
"songs", "song_id", "song_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${performance.song_id}-${performance.artist_id}`,
|
|
||||||
song: createNodeId(`Song-${performance.song_id}`),
|
|
||||||
artist: createNodeId(`Artist-${performance.artist_id}`),
|
|
||||||
as: performance.as
|
|
||||||
}, "Performance", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artistMember of await selectAllFrom(
|
|
||||||
"artist_member", true,
|
|
||||||
"artists", "artist_id", "artist_id",
|
|
||||||
"artists", "member_id", "artist_id",
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${artistMember.artist_id}-${artistMember.member_id}`,
|
|
||||||
group: createNodeId(`Artist-${artistMember.artist_id}`),
|
|
||||||
member: createNodeId(`Artist-${artistMember.member_id}`),
|
|
||||||
as: artistMember.as
|
|
||||||
}, "ArtistMembership", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const animeSeries of await selectAllFrom(
|
|
||||||
"anime_series", true,
|
|
||||||
"anime", "anime_id", "anime_id",
|
|
||||||
"series", "series_id", "series_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${animeSeries.anime_id}-${animeSeries.series_id}`,
|
|
||||||
anime: createNodeId(`Anime-${animeSeries.anime_id}`),
|
|
||||||
series: createNodeId(`Series-${animeSeries.series_id}`)
|
|
||||||
}, "AnimeSeries", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const animeResource of await selectAllFrom(
|
|
||||||
"anime_resource", true,
|
|
||||||
"anime", "anime_id", "anime_id",
|
|
||||||
"resources", "resource_id", "resource_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${animeResource.anime_id}-${animeResource.resource_id}`,
|
|
||||||
anime: createNodeId(`Anime-${animeResource.anime_id}`),
|
|
||||||
resource: createNodeId(`Resource-${animeResource.resource_id}`)
|
|
||||||
}, "AnimeResource", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const animeImage of await selectAllFrom(
|
|
||||||
"anime_image", true,
|
|
||||||
"anime", "anime_id", "anime_id",
|
|
||||||
"images", "image_id", "image_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${animeImage.anime_id}-${animeImage.image_id}`,
|
|
||||||
anime: createNodeId(`Anime-${animeImage.anime_id}`),
|
|
||||||
image: createNodeId(`Image-${animeImage.image_id}`)
|
|
||||||
}, "AnimeImage", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artistResource of await selectAllFrom(
|
|
||||||
"artist_resource", true,
|
|
||||||
"artists", "artist_id", "artist_id",
|
|
||||||
"resources", "resource_id", "resource_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${artistResource.artist_id}-${artistResource.resource_id}`,
|
|
||||||
artist: createNodeId(`Artist-${artistResource.artist_id}`),
|
|
||||||
resource: createNodeId(`Resource-${artistResource.resource_id}`)
|
|
||||||
}, "ArtistResource", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artistImage of await selectAllFrom(
|
|
||||||
"artist_image", true,
|
|
||||||
"artists", "artist_id", "artist_id",
|
|
||||||
"images", "image_id", "image_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${artistImage.artist_id}-${artistImage.image_id}`,
|
|
||||||
artist: createNodeId(`Artist-${artistImage.artist_id}`),
|
|
||||||
image: createNodeId(`Image-${artistImage.image_id}`)
|
|
||||||
}, "ArtistImage", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entryVideo of await selectAllFrom(
|
|
||||||
"anime_theme_entry_video", true,
|
|
||||||
"anime_theme_entries", "entry_id", "entry_id",
|
|
||||||
"videos", "video_id", "video_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${entryVideo.entry_id}-${entryVideo.video_id}`,
|
|
||||||
entry: createNodeId(`Entry-${entryVideo.entry_id}`),
|
|
||||||
video: createNodeId(`Video-${entryVideo.video_id}`)
|
|
||||||
}, "EntryVideo", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const animeStudio of await selectAllFrom(
|
|
||||||
"anime_studio", true,
|
|
||||||
"anime", "anime_id", "anime_id",
|
|
||||||
"studios", "studio_id", "studio_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${animeStudio.anime_id}-${animeStudio.studio_id}`,
|
|
||||||
anime: createNodeId(`Anime-${animeStudio.anime_id}`),
|
|
||||||
studio: createNodeId(`Studio-${animeStudio.studio_id}`)
|
|
||||||
}, "AnimeStudio", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const studioResource of await selectAllFrom(
|
|
||||||
"studio_resource", true,
|
|
||||||
"studios", "studio_id", "studio_id",
|
|
||||||
"resources", "resource_id", "resource_id"
|
|
||||||
)) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${studioResource.studio_id}-${studioResource.resource_id}`,
|
|
||||||
studio: createNodeId(`Studio-${studioResource.studio_id}`),
|
|
||||||
resource: createNodeId(`Resource-${studioResource.resource_id}`)
|
|
||||||
}, "StudioResource", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// await cache.set("last-fetched", now);
|
|
||||||
};
|
|
||||||
|
|
||||||
function createNodeFromData(item, nodeType, helpers) {
|
|
||||||
const node = {
|
|
||||||
...item,
|
|
||||||
id: helpers.createNodeId(`${nodeType}-${item.id}`),
|
|
||||||
parent: null,
|
|
||||||
children: [],
|
|
||||||
internal: {
|
|
||||||
type: nodeType,
|
|
||||||
content: JSON.stringify(item),
|
|
||||||
contentDigest: helpers.createContentDigest(item)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
helpers.createNode(node)
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// function isPluginNode(node) {
|
|
||||||
// return node.internal.owner === "gatsby-source-animethemes";
|
|
||||||
// }
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
const { fetchAnimeList } = require("./src/anime");
|
|
||||||
const { fetchSynonymList } = require("./src/synonym");
|
|
||||||
const { fetchThemeList } = require("./src/theme");
|
|
||||||
const { fetchEntryList } = require("./src/entry");
|
|
||||||
const { fetchVideoList } = require("./src/video");
|
|
||||||
const { fetchSongList } = require("./src/song");
|
|
||||||
const { fetchArtistList } = require("./src/artist");
|
|
||||||
const { fetchSeriesList } = require("./src/series");
|
|
||||||
const { fetchResourceList } = require("./src/resource");
|
|
||||||
const { fetchImageList } = require("./src/image");
|
|
||||||
const { fetchAnnouncements } = require("./src/announcement");
|
|
||||||
|
|
||||||
exports.onPreInit = ({ reporter }) => reporter.info("Loaded gatsby-source-animethemes");
|
|
||||||
|
|
||||||
exports.createSchemaCustomization = ({ actions }) => {
|
|
||||||
const { createTypes } = actions;
|
|
||||||
createTypes(`
|
|
||||||
type Anime implements Node {
|
|
||||||
id: ID!
|
|
||||||
name: String!
|
|
||||||
slug: String!
|
|
||||||
year: Int!
|
|
||||||
season: String
|
|
||||||
synopsis: String
|
|
||||||
synonyms: [Synonym] @link(by: "anime.id", from: "id")
|
|
||||||
themes: [Theme] @link(by: "anime.id", from: "id")
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
series: [Series]
|
|
||||||
resources: [Resource]
|
|
||||||
images: [Image]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Synonym implements Node {
|
|
||||||
id: ID!
|
|
||||||
text: String!
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Theme implements Node {
|
|
||||||
id: ID!
|
|
||||||
slug: String!
|
|
||||||
group: String
|
|
||||||
song: Song! @link(by: "id")
|
|
||||||
anime: Anime! @link(by: "id")
|
|
||||||
entries: [Entry] @link(by: "theme.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry implements Node {
|
|
||||||
id: ID!
|
|
||||||
version: Int
|
|
||||||
episodes: String
|
|
||||||
nsfw: Boolean!
|
|
||||||
spoiler: Boolean!
|
|
||||||
theme: Theme! @link(by: "id")
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
videos: [Video]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Video implements Node {
|
|
||||||
id: ID!
|
|
||||||
filename: String!
|
|
||||||
basename: String!
|
|
||||||
link: String!
|
|
||||||
resolution: Int
|
|
||||||
nc: Boolean!
|
|
||||||
subbed: Boolean!
|
|
||||||
lyrics: Boolean!
|
|
||||||
uncen: Boolean!
|
|
||||||
source: String
|
|
||||||
overlap: String
|
|
||||||
tags: String
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
entries: [Entry]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Song implements Node {
|
|
||||||
id: ID!
|
|
||||||
title: String!
|
|
||||||
themes: [Theme] @link(by: "song.id", from: "id")
|
|
||||||
performances: [Performance] @link(by: "song.id", from: "id")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Performance implements Node {
|
|
||||||
id: ID!
|
|
||||||
song: Song! @link(by: "id")
|
|
||||||
artist: Artist! @link(by: "id")
|
|
||||||
as: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Artist implements Node {
|
|
||||||
id: ID!
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
performances: [Performance] @link(by: "artist.id", from: "id")
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
resources: [Resource]
|
|
||||||
images: [Image]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Series implements Node {
|
|
||||||
id: ID!
|
|
||||||
slug: String!
|
|
||||||
name: String!
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
anime: [Anime]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Resource implements Node {
|
|
||||||
link: String!
|
|
||||||
site: String!
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
anime: [Anime]
|
|
||||||
artists: [Artist]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Image implements Node {
|
|
||||||
facet: String!
|
|
||||||
link: String!
|
|
||||||
|
|
||||||
# See custom resolver below (n-to-n relations cannot be resolved with @link)
|
|
||||||
anime: [Anime]
|
|
||||||
artists: [Artist]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Announcement implements Node {
|
|
||||||
id: ID!
|
|
||||||
content: String!
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.sourceNodes = async ({ cache, actions, getNodes, createNodeId, createContentDigest, reporter }) => {
|
|
||||||
const helpers = {
|
|
||||||
...actions,
|
|
||||||
createNodeId,
|
|
||||||
createContentDigest
|
|
||||||
};
|
|
||||||
|
|
||||||
// const lastFetched = await cache.get("last-fetched");
|
|
||||||
//
|
|
||||||
// getNodes().filter(isPluginNode).forEach((node) => helpers.touchNode(node));
|
|
||||||
//
|
|
||||||
// const now = new Date().toISOString().replace("Z", "000");
|
|
||||||
|
|
||||||
const animeList = await fetchAnimeList({ reporter });
|
|
||||||
const synonymList = await fetchSynonymList({ reporter });
|
|
||||||
const themeList = await fetchThemeList({ reporter });
|
|
||||||
const entryList = await fetchEntryList({ reporter });
|
|
||||||
const videoList = await fetchVideoList({ reporter });
|
|
||||||
const songList = await fetchSongList({ reporter });
|
|
||||||
const artistList = await fetchArtistList({ reporter });
|
|
||||||
const seriesList = await fetchSeriesList({ reporter });
|
|
||||||
const resourceList = await fetchResourceList({ reporter });
|
|
||||||
const imageList = await fetchImageList({ reporter });
|
|
||||||
|
|
||||||
for (const anime of animeList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: anime.id,
|
|
||||||
name: anime.name,
|
|
||||||
slug: anime.slug,
|
|
||||||
year: anime.year,
|
|
||||||
season: anime.season,
|
|
||||||
synopsis: anime.synopsis,
|
|
||||||
series: anime.series.map((series) => createNodeId(`Series-${series.id}`)),
|
|
||||||
themes: anime.themes.map((theme) => createNodeId(`Theme-${theme.id}`)),
|
|
||||||
resources: anime.resources.map((resource) => createNodeId(`Resource-${resource.id}`)),
|
|
||||||
images: anime.images.map((image) => createNodeId(`Image-${image.id}`))
|
|
||||||
}, "Anime", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const synonym of synonymList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: synonym.id,
|
|
||||||
text: synonym.text,
|
|
||||||
anime: createNodeId(`Anime-${synonym.anime.id}`)
|
|
||||||
}, "Synonym", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const theme of themeList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: theme.id,
|
|
||||||
slug: theme.slug,
|
|
||||||
group: theme.group,
|
|
||||||
song: createNodeId(`Song-${theme.song.id}`),
|
|
||||||
anime: createNodeId(`Anime-${theme.anime.id}`),
|
|
||||||
entries: theme.entries.map((entry) => createNodeId(`Entry-${entry.id}`))
|
|
||||||
}, "Theme", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of entryList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: entry.id,
|
|
||||||
version: entry.version || 1,
|
|
||||||
episodes: entry.episodes,
|
|
||||||
nsfw: entry.nsfw,
|
|
||||||
spoiler: entry.spoiler,
|
|
||||||
theme: createNodeId(`Theme-${entry.theme.id}`),
|
|
||||||
videos: entry.videos.map((video) => createNodeId(`Video-${video.id}`))
|
|
||||||
}, "Entry", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const video of videoList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: video.id,
|
|
||||||
filename: video.filename,
|
|
||||||
basename: video.basename,
|
|
||||||
link: video.link,
|
|
||||||
resolution: video.resolution,
|
|
||||||
nc: video.nc,
|
|
||||||
subbed: video.subbed,
|
|
||||||
lyrics: video.lyrics,
|
|
||||||
uncen: video.uncen,
|
|
||||||
source: video.source,
|
|
||||||
overlap: video.overlap,
|
|
||||||
tags: video.tags,
|
|
||||||
entries: video.entries.map((entry) => createNodeId(`Entry-${entry.id}`))
|
|
||||||
}, "Video", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const song of songList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: song.id,
|
|
||||||
title: song.title,
|
|
||||||
themes: song.themes.map((theme) => createNodeId(`Theme-${theme.id}`)),
|
|
||||||
performances: song.artists.map((artist) => createNodeId(`Performance-${song.id}-${artist.id}`))
|
|
||||||
}, "Song", helpers);
|
|
||||||
|
|
||||||
for (const artist of song.artists) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: `${song.id}-${artist.id}`,
|
|
||||||
song: createNodeId(`Song-${song.id}`),
|
|
||||||
artist: createNodeId(`Artist-${artist.id}`),
|
|
||||||
as: artist.as
|
|
||||||
}, "Performance", helpers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const artist of artistList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: artist.id,
|
|
||||||
slug: artist.slug,
|
|
||||||
name: artist.name,
|
|
||||||
performances: artist.songs.map((song) => createNodeId(`Performance-${song.id}-${artist.id}`)),
|
|
||||||
resources: artist.resources.map((resource) => createNodeId(`Resource-${resource.id}`)),
|
|
||||||
images: artist.images.map((image) => createNodeId(`Image-${image.id}`))
|
|
||||||
}, "Artist", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const series of seriesList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: series.id,
|
|
||||||
slug: series.slug,
|
|
||||||
name: series.name,
|
|
||||||
anime: series.anime.map((anime) => createNodeId(`Anime-${anime.id}`))
|
|
||||||
}, "Series", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const resource of resourceList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: resource.id,
|
|
||||||
link: resource.link,
|
|
||||||
site: resource.site,
|
|
||||||
anime: resource.anime.map((anime) => createNodeId(`Anime-${anime.id}`)),
|
|
||||||
artists: resource.artists.map((artist) => createNodeId(`Artist-${artist.id}`))
|
|
||||||
}, "Resource", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const image of imageList) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: image.id,
|
|
||||||
facet: image.facet,
|
|
||||||
link: image.link,
|
|
||||||
anime: image.anime.map((anime) => createNodeId(`Anime-${anime.id}`)),
|
|
||||||
artists: image.artists.map((artist) => createNodeId(`Artist-${artist.id}`))
|
|
||||||
}, "Image", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
const announcements = await fetchAnnouncements();
|
|
||||||
|
|
||||||
for (const announcement of announcements) {
|
|
||||||
createNodeFromData({
|
|
||||||
id: announcement.id,
|
|
||||||
content: announcement.content
|
|
||||||
}, "Announcement", helpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// await cache.set("last-fetched", now);
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.createResolvers = ({ createResolvers }) => {
|
|
||||||
createResolvers({
|
|
||||||
Anime: {
|
|
||||||
series: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getNodesByIds({ ids: source.series, type: "Series" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
images: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getAllNodes({ type: "Image" }).filter((node) => node.anime.includes(source.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getAllNodes({ type: "Resource" }).filter((node) => node.anime.includes(source.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Artist: {
|
|
||||||
images: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getAllNodes({ type: "Resource" }).filter((node) => node.artists.includes(source.id));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resources: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getAllNodes({ type: "Resource" }).filter((node) => node.artists.includes(source.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Series: {
|
|
||||||
anime: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getNodesByIds({ ids: source.anime, type: "Anime" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Entry: {
|
|
||||||
videos: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getNodesByIds({ ids: source.videos, type: "Video" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Video: {
|
|
||||||
entries: {
|
|
||||||
resolve(source, args, context) {
|
|
||||||
return context.nodeModel.getNodesByIds({ ids: source.entries, type: "Entry" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function createNodeFromData(item, nodeType, helpers) {
|
|
||||||
const node = {
|
|
||||||
...item,
|
|
||||||
id: helpers.createNodeId(`${nodeType}-${item.id}`),
|
|
||||||
parent: null,
|
|
||||||
children: [],
|
|
||||||
internal: {
|
|
||||||
type: nodeType,
|
|
||||||
content: JSON.stringify(item),
|
|
||||||
contentDigest: helpers.createContentDigest(item)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
helpers.createNode(node)
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// function isPluginNode(node) {
|
|
||||||
// return node.internal.owner === "gatsby-source-animethemes";
|
|
||||||
// }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "gatsby-source-animethemes",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
anime: [ "id", "name", "slug", "year", "season", "synopsis" ],
|
|
||||||
image: [ "id" ],
|
|
||||||
theme: [ "id" ],
|
|
||||||
series: [ "id" ],
|
|
||||||
resource: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchAnimeList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching anime list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const animeList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/anime?page[size]=100&sort=year,season,name&${fields}&include=images,themes,series,externalResources&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.anime,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} anime fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return animeList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchAnimeList
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const { baseUrl, fetchJson } = require("./index");
|
|
||||||
|
|
||||||
function fetchAnnouncements() {
|
|
||||||
return fetchJson(`${baseUrl}/api/announcement`)
|
|
||||||
.then((json) => json.announcements);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchAnnouncements
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
artist: [ "id", "slug", "name" ],
|
|
||||||
song: [ "id" ],
|
|
||||||
resource: [ "id" ],
|
|
||||||
image: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchArtistList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching artist list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const artistList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/artist?page[size]=100&sort=name&${fields}&include=songs,externalResources,images&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.artists,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} artists fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return artistList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchArtistList
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
entry: [ "id", "version", "episodes", "nsfw", "spoiler" ],
|
|
||||||
theme: [ "id" ],
|
|
||||||
video: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchEntryList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching entry list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const entryList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/entry?page[size]=100&${fields}&include=theme,videos&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.entries,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} entries fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return entryList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchEntryList
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
image: [ "id", "facet", "link" ],
|
|
||||||
anime: [ "id" ],
|
|
||||||
artist: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchImageList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching image list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const imageList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/image?page[size]=100&${fields}&include=anime,artists&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.images,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} images fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return imageList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchImageList
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
const fetch = require("node-fetch");
|
|
||||||
|
|
||||||
const baseUrl = process.env.GATSBY_API_URL || "https://staging.animethemes.moe";
|
|
||||||
|
|
||||||
const requestCooldown = 1500;
|
|
||||||
let lastRequest;
|
|
||||||
|
|
||||||
async function fetchJson(url, init) {
|
|
||||||
// Debounce, we should only request once every second
|
|
||||||
if (lastRequest) {
|
|
||||||
const expiredTime = Date.now() - lastRequest;
|
|
||||||
if (expiredTime < requestCooldown) {
|
|
||||||
await sleep(requestCooldown - expiredTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRequest = Date.now();
|
|
||||||
|
|
||||||
const response = await fetch(url, init);
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchJsonPaginated(url, { reducer = (page) => page, flatten, onProgress, ...init }) {
|
|
||||||
const results = [];
|
|
||||||
let nextUrl = url;
|
|
||||||
|
|
||||||
console.log(`URL schema: ${nextUrl}`);
|
|
||||||
|
|
||||||
while (nextUrl) {
|
|
||||||
const page = await fetchJson(nextUrl, init);
|
|
||||||
const pageResults = reducer(page);
|
|
||||||
|
|
||||||
if (flatten) {
|
|
||||||
results.push(...pageResults);
|
|
||||||
} else {
|
|
||||||
results.push(pageResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextUrl = page.links.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFieldParams(fields) {
|
|
||||||
return Object.entries(fields)
|
|
||||||
.map(([ key, values ]) => `fields[${key}]=${values.join()}`)
|
|
||||||
.join("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(millis) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
baseUrl,
|
|
||||||
fetchJson,
|
|
||||||
fetchJsonPaginated,
|
|
||||||
createFieldParams
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
resource: [ "id", "link", "site" ],
|
|
||||||
anime: [ "id" ],
|
|
||||||
artist: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchResourceList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching resource list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const resourceList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/resource?page[size]=100&${fields}&include=anime,artists&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.resources,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} resources fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return resourceList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchResourceList
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
series: [ "id", "slug", "name" ],
|
|
||||||
anime: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchSeriesList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching series list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const seriesList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/series?page[size]=100&sort=name&${fields}&include=anime&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.series,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} series fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return seriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchSeriesList
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
song: [ "id", "title" ],
|
|
||||||
theme: [ "id" ],
|
|
||||||
artist: [ "id", "as" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchSongList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching song list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const songList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/song?page[size]=100&${fields}&include=themes,artists&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.songs,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} songs fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return songList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchSongList
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
synonym: [ "id", "text" ],
|
|
||||||
anime: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchSynonymList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching synonym list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const synonymList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/synonym?page[size]=100&${fields}&include=anime&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.synonyms,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} synonyms fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return synonymList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchSynonymList
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
theme: [ "id", "slug", "group" ],
|
|
||||||
anime: [ "id" ],
|
|
||||||
song: [ "id" ],
|
|
||||||
entry: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchThemeList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching theme list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const themeList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/theme?page[size]=100&${fields}&include=anime,song,entries&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.themes,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} themes fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return themeList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchThemeList
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
const { baseUrl, fetchJsonPaginated, createFieldParams } = require("./index");
|
|
||||||
|
|
||||||
const fields = createFieldParams({
|
|
||||||
video: [ "id", "filename", "basename", "link", "resolution", "nc", "subbed", "lyrics", "uncen", "source", "overlap", "tags" ],
|
|
||||||
entry: [ "id" ]
|
|
||||||
});
|
|
||||||
|
|
||||||
async function fetchVideoList({ reporter, lastFetched }) {
|
|
||||||
const activity = reporter.activityTimer("Fetching video list");
|
|
||||||
activity.start();
|
|
||||||
|
|
||||||
const videoList = await fetchJsonPaginated(
|
|
||||||
`${baseUrl}/api/video?page[size]=100&${fields}&include=entries&filter[updated_at][gte]=${lastFetched}`,
|
|
||||||
{
|
|
||||||
reducer: (page) => page.videos,
|
|
||||||
flatten: true,
|
|
||||||
onProgress: (results) => activity.setStatus(`${results.length} videos fetched`)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
activity.end();
|
|
||||||
|
|
||||||
return videoList;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchVideoList
|
|
||||||
};
|
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
|
After Width: | Height: | Size: 507 B |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 301 B After Width: | Height: | Size: 301 B |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="1000.000000pt" height="1000.000000pt" viewBox="0 0 1000.000000 1000.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,1000.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M2953 6087 c-1030 -595 -1873 -1084 -1873 -1087 0 -3 210 -126 467
|
||||||
|
-274 l467 -269 30 17 c17 9 438 252 936 539 l905 523 3 -1081 2 -1081 454
|
||||||
|
-262 c249 -144 460 -264 470 -268 15 -6 16 152 16 2160 0 1191 -1 2166 -2
|
||||||
|
2165 -2 0 -846 -487 -1875 -1082z"/>
|
||||||
|
<path d="M5330 4999 c0 -1353 4 -2159 9 -2157 6 2 217 122 470 268 l461 265 0
|
||||||
|
1078 c0 592 2 1077 5 1077 3 0 421 -239 927 -532 507 -293 929 -532 938 -532
|
||||||
|
18 0 930 523 930 534 0 7 -3723 2160 -3734 2160 -3 0 -6 -972 -6 -2161z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1004 B |
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
// Basic site metadata
|
|
||||||
siteName: "AnimeThemes",
|
|
||||||
description: "AnimeThemes is a simple and consistent repository of anime opening and ending themes.",
|
|
||||||
siteLanguage: "en",
|
|
||||||
|
|
||||||
// Prefix for all links (for production builds)
|
|
||||||
pathPrefix: "/animethemes",
|
|
||||||
|
|
||||||
// Site URL without pathPrefix
|
|
||||||
rootUrl: "https://animethemes.github.io"
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { VideoTags } from "components/utils";
|
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { VideoTags } from "components/utils";
|
||||||
import PlayerContext from "context/playerContext";
|
import PlayerContext from "context/playerContext";
|
||||||
import createVideoSlug from "utils/createVideoSlug";
|
import createVideoSlug from "utils/createVideoSlug";
|
||||||
import { Link } from "gatsby";
|
|
||||||
import { Icon } from "components/icon";
|
import { Icon } from "components/icon";
|
||||||
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
|
|
||||||
export function VideoButton({ anime, theme, entry, video, ...props }) {
|
export function VideoButton({ anime, theme, entry, video, ...props }) {
|
||||||
@@ -13,8 +13,8 @@ export function VideoButton({ anime, theme, entry, video, ...props }) {
|
|||||||
const isPlaying = currentVideo && currentVideo.filename === video.filename;
|
const isPlaying = currentVideo && currentVideo.filename === video.filename;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/anime/${anime.slug}/${videoSlug}`}>
|
<Link href={`/anime/${anime.slug}/${videoSlug}`} passHref>
|
||||||
<Button {...props}>
|
<Button as="a" {...props}>
|
||||||
<Button as="span" variant="primary">
|
<Button as="span" variant="primary">
|
||||||
<Icon icon={isPlaying ? faCompactDisc : faPlay} spin={isPlaying}/>
|
<Icon icon={isPlaying ? faCompactDisc : faPlay} spin={isPlaying}/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { Button } from "./Button";
|
export { Button } from "./Button";
|
||||||
export { VideoButton } from "./VideoButton"
|
export { VideoButton } from "./VideoButton";
|
||||||
export { FilterToggleButton } from "./FilterToggleButton";
|
export { FilterToggleButton } from "./FilterToggleButton";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { graphql, Link } from "gatsby";
|
import Link from "next/link";
|
||||||
import { faEllipsisH, faPlay } from "@fortawesome/free-solid-svg-icons";
|
import { faEllipsisH, faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
@@ -7,7 +7,7 @@ import createVideoSlug from "utils/createVideoSlug";
|
|||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
import { Icon } from "components/icon";
|
import { Icon } from "components/icon";
|
||||||
import { SummaryCard } from "components/card";
|
import { SummaryCard } from "components/card";
|
||||||
import { chain, themeSequenceComparator, themeTypeComparator } from "utils/comparators";
|
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||||
|
|
||||||
export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||||
const { smallCover } = useImage(anime);
|
const { smallCover } = useImage(anime);
|
||||||
@@ -25,8 +25,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
|||||||
<SummaryCard.Description>
|
<SummaryCard.Description>
|
||||||
<span>Anime</span>
|
<span>Anime</span>
|
||||||
{!!anime.year && (
|
{!!anime.year && (
|
||||||
<Link to={premiereLink}>
|
<Link href={premiereLink} passHref>
|
||||||
<Text link>{premiere}</Text>
|
<Text as="a" link>{premiere}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<span>{anime.themes.length} themes</span>
|
<span>{anime.themes.length} themes</span>
|
||||||
@@ -41,10 +41,10 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
|||||||
to={animeLink}
|
to={animeLink}
|
||||||
>
|
>
|
||||||
{!hideThemes && (
|
{!hideThemes && (
|
||||||
<Flex display={[ "none", "flex" ]} flexWrap="wrap" gapsBoth="0.75rem">
|
<Flex display={["none", "flex"]} flexWrap="wrap" gapsBoth="0.75rem">
|
||||||
{anime.themes
|
{anime.themes
|
||||||
.filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
|
.filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
|
||||||
.sort(chain(themeTypeComparator, themeSequenceComparator))
|
.sort(chain(themeTypeComparator, themeIndexComparator))
|
||||||
.slice(0, maxThemes)
|
.slice(0, maxThemes)
|
||||||
.map((theme) => {
|
.map((theme) => {
|
||||||
const entry = theme.entries[0];
|
const entry = theme.entries[0];
|
||||||
@@ -52,8 +52,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
|||||||
const videoSlug = createVideoSlug(theme, entry, video);
|
const videoSlug = createVideoSlug(theme, entry, video);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link key={theme.slug} to={`/anime/${anime.slug}/${videoSlug}`}>
|
<Link key={theme.slug} href={`/anime/${anime.slug}/${videoSlug}`} passHref>
|
||||||
<Button variant="on-card">
|
<Button as="a" variant="on-card">
|
||||||
<Button as="span" variant="primary">
|
<Button as="span" variant="primary">
|
||||||
<Icon icon={faPlay}/>
|
<Icon icon={faPlay}/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -64,8 +64,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
{anime.themes.length > 4 && (
|
{anime.themes.length > 4 && (
|
||||||
<Link to={animeLink}>
|
<Link href={animeLink} passHref>
|
||||||
<Button variant="on-card" title="Show all themes">
|
<Button as="a" variant="on-card" title="Show all themes">
|
||||||
<Icon icon={faEllipsisH}/>
|
<Icon icon={faEllipsisH}/>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -75,31 +75,3 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
|||||||
</SummaryCard>
|
</SummaryCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const query = graphql`
|
|
||||||
fragment AnimeCard on Anime {
|
|
||||||
name
|
|
||||||
slug
|
|
||||||
year
|
|
||||||
season
|
|
||||||
themes {
|
|
||||||
type
|
|
||||||
sequence
|
|
||||||
slug
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
site
|
|
||||||
link
|
|
||||||
}
|
|
||||||
images {
|
|
||||||
facet
|
|
||||||
link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment AnimeCardThemes on Anime {
|
|
||||||
themes {
|
|
||||||
...VideoSlug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,53 +1,65 @@
|
|||||||
import { Link, withPrefix } from "gatsby";
|
import Link from "next/link";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import styled, { css, keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components";
|
||||||
import { Card } from "components/card";
|
import { Card } from "components/card";
|
||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
|
import withBasePath from "utils/withBasePath";
|
||||||
|
|
||||||
const loadingAnimation = keyframes`
|
const loadingAnimation = keyframes`
|
||||||
0% { background-position: 0 0; }
|
0% {
|
||||||
100% { background-position: 100% 100%; }
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledCover = styled.img.attrs({
|
const StyledCover = styled.img.attrs({
|
||||||
loading: "lazy"
|
loading: "lazy"
|
||||||
})`
|
})`
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
||||||
background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat;
|
background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat;
|
||||||
background-size: 500% 500%;
|
background-size: 500% 500%;
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
animation: ${loadingAnimation} 2s infinite alternate linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
${(props) => props.placeholder && css`
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
padding: 0.5rem;
|
animation: ${loadingAnimation} 2s infinite alternate linear;
|
||||||
object-fit: contain;
|
}
|
||||||
background: white;
|
|
||||||
`}
|
${(props) => props.isPlaceholder && css`
|
||||||
|
padding: 0.5rem;
|
||||||
|
object-fit: contain;
|
||||||
|
background: white;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function SummaryCard({ title, description, image, to, children, ...props }) {
|
export function SummaryCard({ title, description, image, to, children, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}>
|
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}>
|
||||||
<Link to={to}>
|
<Link href={to}>
|
||||||
<StyledCover alt="Cover" src={image || withPrefix("/img/logo.svg")} placeholder={!image} loading="lazy"/>
|
<a>
|
||||||
|
<StyledCover
|
||||||
|
alt="Cover"
|
||||||
|
src={image || withBasePath("/img/logo.svg")}
|
||||||
|
isPlaceholder={!image}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
<Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem">
|
<Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem">
|
||||||
<Text fontWeight="600" maxLines={1}>
|
<Text fontWeight="600" maxLines={1}>
|
||||||
{typeof title === "string" ? (
|
{typeof title === "string" ? (
|
||||||
<Link to={to}>
|
<Link href={to} passHref>
|
||||||
<Text link>{title}</Text>
|
<Text as="a" link>{title}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
) : title}
|
) : title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="small" maxLines={1}>
|
<Text variant="small" maxLines={1}>
|
||||||
{typeof description === "string" ? (
|
{typeof description === "string" ? (
|
||||||
<SummaryCard.Description>
|
<SummaryCard.Description>
|
||||||
{[ description ]}
|
{[description]}
|
||||||
</SummaryCard.Description>
|
</SummaryCard.Description>
|
||||||
) : description}
|
) : description}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -70,4 +82,4 @@ SummaryCard.Description = function SummaryCardDescription({ children }) {
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,24 @@
|
|||||||
import { Link } from "gatsby";
|
import Link from "next/link";
|
||||||
import { SongTitleWithArtists } from "components/utils";
|
import { SongTitleWithArtists } from "components/utils";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import useImage from "hooks/useImage";
|
import useImage from "hooks/useImage";
|
||||||
import createVideoSlug from "utils/createVideoSlug";
|
import createVideoSlug from "utils/createVideoSlug";
|
||||||
import { SummaryCard } from "components/card";
|
import { SummaryCard } from "components/card";
|
||||||
|
import { faEllipsisV } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { Icon } from "components/icon";
|
||||||
|
import { Button } from "components/button";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const StyledSummaryCard = styled(SummaryCard)``;
|
||||||
|
const StyledShowOnHover = styled.div`
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
${StyledSummaryCard}:hover & {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
// Specify an artist if you want to display this in an artist context (e.g. artist page)
|
// Specify an artist if you want to display this in an artist context (e.g. artist page)
|
||||||
export function ThemeSummaryCard({ theme, artist }) {
|
export function ThemeSummaryCard({ theme, artist }) {
|
||||||
@@ -27,18 +42,24 @@ export function ThemeSummaryCard({ theme, artist }) {
|
|||||||
<SummaryCard.Description>
|
<SummaryCard.Description>
|
||||||
<span>Theme</span>
|
<span>Theme</span>
|
||||||
<span>{theme.slug}</span>
|
<span>{theme.slug}</span>
|
||||||
<Link to={`/anime/${theme.anime.slug}`}>
|
<Link href={`/anime/${theme.anime.slug}`} passHref>
|
||||||
<Text link>{theme.anime.name}</Text>
|
<Text as="a" link>{theme.anime.name}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
</SummaryCard.Description>
|
</SummaryCard.Description>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SummaryCard
|
<StyledSummaryCard
|
||||||
title={<SongTitleWithArtists song={theme.song} songTitleLinkTo={to} artist={artist}/>}
|
title={<SongTitleWithArtists song={theme.song} songTitleLinkTo={to} artist={artist}/>}
|
||||||
description={description}
|
description={description}
|
||||||
image={smallCover}
|
image={smallCover}
|
||||||
to={to}
|
to={to}
|
||||||
/>
|
>
|
||||||
|
<StyledShowOnHover>
|
||||||
|
<Button variant="on-card" circle silent>
|
||||||
|
<Icon icon={faEllipsisV}/>
|
||||||
|
</Button>
|
||||||
|
</StyledShowOnHover>
|
||||||
|
</StyledSummaryCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { Card } from "./Card";
|
export { Card } from "./Card";
|
||||||
|
export { SummaryCard } from "./SummaryCard";
|
||||||
export { AnimeSummaryCard } from "./AnimeSummaryCard";
|
export { AnimeSummaryCard } from "./AnimeSummaryCard";
|
||||||
export { ThemeSummaryCard } from "./ThemeSummaryCard";
|
export { ThemeSummaryCard } from "./ThemeSummaryCard";
|
||||||
export { ArtistSummaryCard } from "./ArtistSummaryCard";
|
export { ArtistSummaryCard } from "./ArtistSummaryCard";
|
||||||
export { SummaryCard } from "./SummaryCard";
|
|
||||||
export { ThemeDetailCard } from "./ThemeDetailCard";
|
export { ThemeDetailCard } from "./ThemeDetailCard";
|
||||||
export { ErrorCard } from "./ErrorCard";
|
export { ErrorCard } from "./ErrorCard";
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ DescriptionList.Item = function DescriptionListItem({ title, children }) {
|
|||||||
<StyledValue>{children}</StyledValue>
|
<StyledValue>{children}</StyledValue>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Box, Flex } from "components/box";
|
|||||||
import { ThemeDetailCard } from "components/card";
|
import { ThemeDetailCard } from "components/card";
|
||||||
import { Listbox } from "components/listbox";
|
import { Listbox } from "components/listbox";
|
||||||
import { HorizontalScroll } from "components/utils";
|
import { HorizontalScroll } from "components/utils";
|
||||||
import { chain, themeSequenceComparator, themeTypeComparator } from "utils/comparators";
|
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||||
|
|
||||||
export function AnimeThemeFilter({ themes }) {
|
export function AnimeThemeFilter({ themes }) {
|
||||||
const groups = useMemo(() => themes.reduce((groups, theme) => {
|
const groups = useMemo(() => themes.reduce((groups, theme) => {
|
||||||
@@ -28,7 +28,7 @@ export function AnimeThemeFilter({ themes }) {
|
|||||||
|
|
||||||
const filteredThemes = activeThemes
|
const filteredThemes = activeThemes
|
||||||
.filter((theme) => !filterType || theme.type === filterType)
|
.filter((theme) => !filterType || theme.type === filterType)
|
||||||
.sort(chain(themeTypeComparator, themeSequenceComparator));
|
.sort(chain(themeTypeComparator, themeIndexComparator));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box gapsColumn="1rem">
|
<Box gapsColumn="1rem">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const StyledSearchInput = styled.div`
|
|||||||
`;
|
`;
|
||||||
const StyledForm = styled.form`
|
const StyledForm = styled.form`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`
|
`;
|
||||||
const StyledInput = styled.input`
|
const StyledInput = styled.input`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { Link, withPrefix } from "gatsby";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
faBars,
|
faBars,
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
@@ -22,14 +22,15 @@ import ColorThemeContext from "context/colorThemeContext";
|
|||||||
import { Icon } from "components/icon";
|
import { Icon } from "components/icon";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
import { useLocation } from "@reach/router";
|
|
||||||
import useCurrentSeason from "hooks/useCurrentSeason";
|
import useCurrentSeason from "hooks/useCurrentSeason";
|
||||||
import navigateToRandomTheme from "utils/navigateToRandomTheme";
|
import navigateToRandomTheme from "utils/navigateToRandomTheme";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import withBasePath from "utils/withBasePath";
|
||||||
|
|
||||||
export function Navigation({ offsetToggleButton = false }) {
|
export function Navigation({ offsetToggleButton = false }) {
|
||||||
const [ show, setShow ] = useState(false);
|
const [ show, setShow ] = useState(false);
|
||||||
const { colorTheme, toggleColorTheme } = useContext(ColorThemeContext);
|
const { colorTheme, toggleColorTheme } = useContext(ColorThemeContext);
|
||||||
const location = useLocation();
|
const router = useRouter();
|
||||||
|
|
||||||
const { currentYear, currentSeason } = useCurrentSeason();
|
const { currentYear, currentSeason } = useCurrentSeason();
|
||||||
|
|
||||||
@@ -37,28 +38,30 @@ export function Navigation({ offsetToggleButton = false }) {
|
|||||||
// we want to close the modal navigation.
|
// we want to close the modal navigation.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
}, [location]);
|
}, [router.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledNavigation show={show} onClick={() => setShow(false)}>
|
<StyledNavigation show={show} onClick={() => setShow(false)}>
|
||||||
<StyledNavigationContainer onClick={(event) => event.stopPropagation()}>
|
<StyledNavigationContainer onClick={(event) => event.stopPropagation()}>
|
||||||
<StyledLogoContainer to="/">
|
<Link href="/" passHref>
|
||||||
<StyledLogo
|
<StyledLogoContainer>
|
||||||
src={withPrefix("/img/logo.svg")}
|
<StyledLogo
|
||||||
alt="Logo"
|
src={withBasePath("/img/logo.svg")}
|
||||||
width="277"
|
alt="Logo"
|
||||||
height="150"
|
width="277"
|
||||||
/>
|
height="150"
|
||||||
</StyledLogoContainer>
|
/>
|
||||||
|
</StyledLogoContainer>
|
||||||
|
</Link>
|
||||||
<Flex
|
<Flex
|
||||||
flexDirection={[ "column", "row" ]}
|
flexDirection={[ "column", "row" ]}
|
||||||
gapsRow={[ 0, "0.5rem" ]}
|
gapsRow={[ 0, "0.5rem" ]}
|
||||||
gapsColumn={[ "0.5rem", 0 ]}
|
gapsColumn={[ "0.5rem", 0 ]}
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
>
|
>
|
||||||
<Link to="/search">
|
<Link href="/search" passHref>
|
||||||
<Button variant="on-card" silent gapsRow="0.5rem">
|
<Button as="a" variant="on-card" silent gapsRow="0.5rem">
|
||||||
<Icon icon={faSearch}/>
|
<Icon icon={faSearch}/>
|
||||||
<Text>Search</Text>
|
<Text>Search</Text>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -67,8 +70,8 @@ export function Navigation({ offsetToggleButton = false }) {
|
|||||||
<Icon icon={faRandom}/>
|
<Icon icon={faRandom}/>
|
||||||
<Text>Play Random</Text>
|
<Text>Play Random</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Link to={`/year/${currentYear}/${currentSeason}`}>
|
<Link href={(currentYear && currentSeason) ? `/year/${currentYear}/${currentSeason}` : "/"} passHref>
|
||||||
<Button variant="on-card" silent gapsRow="0.5rem">
|
<Button as="a" variant="on-card" silent gapsRow="0.5rem">
|
||||||
<Icon icon={faTv}/>
|
<Icon icon={faTv}/>
|
||||||
<Text>Current Season</Text>
|
<Text>Current Season</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import styled, { css } from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
import { Container } from "components/container";
|
import { Container } from "components/container";
|
||||||
import { gapsColumn, gapsRow } from "styles/mixins";
|
import { gapsColumn, gapsRow } from "styles/mixins";
|
||||||
import { Link } from "gatsby";
|
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import theme from "theme";
|
import theme from "theme";
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export const StyledNavigationContainer = styled(Container)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLogoContainer = styled(Link)`
|
export const StyledLogoContainer = styled.a`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { Link, navigate } from "gatsby";
|
import Link from "next/link";
|
||||||
import { Box, Flex } from "components/box";
|
import { Box, Flex } from "components/box";
|
||||||
import { SEO } from "components/seo";
|
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import { SearchInput } from "components/input";
|
import { SearchInput } from "components/input";
|
||||||
import { HorizontalScroll } from "components/utils";
|
import { HorizontalScroll } from "components/utils";
|
||||||
@@ -10,6 +9,7 @@ import { Icon } from "components/icon";
|
|||||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import theme from "theme";
|
import theme from "theme";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const StyledSearchOptions = styled(Flex)`
|
const StyledSearchOptions = styled(Flex)`
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -20,36 +20,41 @@ const StyledSearchOptions = styled(Flex)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function SearchNavigation({ entity, location: { search, hash, state } }) {
|
export function SearchNavigation() {
|
||||||
const urlParams = useMemo(() => new URLSearchParams(search), [ search ]);
|
const router = useRouter();
|
||||||
const urlSuffix = search + hash;
|
const { entity, ...urlParams } = router.query;
|
||||||
|
const searchQuery = urlParams.q || "";
|
||||||
const [ searchQuery, setSearchQuery ] = useState(urlParams.get("q") || "");
|
|
||||||
|
|
||||||
// Generates page title based on search query
|
// Generates page title based on search query
|
||||||
const pageTitle = searchQuery && searchQuery.trim()
|
const pageTitle = searchQuery && searchQuery.trim()
|
||||||
? `${searchQuery} - Search`
|
? `${searchQuery} - Search`
|
||||||
: "Search";
|
: "Search";
|
||||||
|
|
||||||
useEffect(() => {
|
const updateSearchQuery = (newSearchQuery) => {
|
||||||
// Update URL to maintain the searchQuery on page navigation.
|
// Update URL to maintain the searchQuery on page navigation.
|
||||||
const newUrlParams = new URLSearchParams();
|
const newUrlParams = {
|
||||||
if (searchQuery) {
|
...urlParams
|
||||||
newUrlParams.set("q", searchQuery);
|
};
|
||||||
|
|
||||||
|
if (newSearchQuery) {
|
||||||
|
newUrlParams.q = newSearchQuery;
|
||||||
|
} else {
|
||||||
|
delete newUrlParams.q;
|
||||||
}
|
}
|
||||||
const params = newUrlParams.toString();
|
|
||||||
|
|
||||||
let url = "/search";
|
let url = "/search";
|
||||||
if (entity) {
|
if (entity) {
|
||||||
url += `/${entity}`;
|
url += `/${entity}`;
|
||||||
}
|
}
|
||||||
if (params) {
|
|
||||||
url += `?${params}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(url, { replace: true, state });
|
router.replace({
|
||||||
|
pathname: url,
|
||||||
|
query: newUrlParams
|
||||||
|
}, null, {
|
||||||
|
shallow: true
|
||||||
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ entity, searchQuery ]);
|
};
|
||||||
|
|
||||||
const inputRef = useRef();
|
const inputRef = useRef();
|
||||||
|
|
||||||
@@ -64,13 +69,12 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box gapsColumn="1.5rem">
|
<Box gapsColumn="1.5rem">
|
||||||
<SEO title={pageTitle} />
|
|
||||||
<Text variant="h1">Search</Text>
|
<Text variant="h1">Search</Text>
|
||||||
<StyledSearchOptions>
|
<StyledSearchOptions>
|
||||||
<Box flex="1" minWidth="33%">
|
<Box flex="1" minWidth="33%">
|
||||||
<SearchInput
|
<SearchInput
|
||||||
query={searchQuery}
|
query={searchQuery}
|
||||||
setQuery={setSearchQuery}
|
setQuery={updateSearchQuery}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
ref: inputRef,
|
ref: inputRef,
|
||||||
spellCheck: false
|
spellCheck: false
|
||||||
@@ -84,15 +88,15 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
|
|||||||
>
|
>
|
||||||
{({ Button, item, selected, content }) => item.value === null ? (
|
{({ Button, item, selected, content }) => item.value === null ? (
|
||||||
!!entity ? (
|
!!entity ? (
|
||||||
<Link key={null} to={`/search${urlSuffix}`}>
|
<Link key={null} href={{ pathname: "/search", query: urlParams }} passHref>
|
||||||
<Button>
|
<Button as="a">
|
||||||
<Icon icon={faTimes}/>
|
<Icon icon={faTimes}/>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<Link key={item.value} to={`/search/${item.value}${urlSuffix}`}>
|
<Link key={item.value} href={{ pathname: `/search/${item.value}`, query: urlParams }} passHref>
|
||||||
<Button variant={selected && "primary"}>
|
<Button as="a" variant={selected && "primary"}>
|
||||||
{content}
|
{content}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Box } from "components/box";
|
import { Box } from "components/box";
|
||||||
import { Switcher } from "components/switcher";
|
import { Switcher } from "components/switcher";
|
||||||
import { Link } from "gatsby";
|
import Link from "next/link";
|
||||||
|
|
||||||
export function SeasonNavigation({ year, season, seasonList }) {
|
export function SeasonNavigation({ year, season, seasonList }) {
|
||||||
return (
|
return (
|
||||||
@@ -11,8 +11,8 @@ export function SeasonNavigation({ year, season, seasonList }) {
|
|||||||
mx="auto"
|
mx="auto"
|
||||||
>
|
>
|
||||||
{({ Button, item, selected, content }) => (
|
{({ Button, item, selected, content }) => (
|
||||||
<Link key={item.value} to={`/year/${year}/${item.value}`}>
|
<Link key={item.value} href={`/year/${year}/${item.value}`} passHref>
|
||||||
<Button variant={selected && "primary"}>
|
<Button as="a" variant={selected && "primary"}>
|
||||||
{content}
|
{content}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { graphql, Link, useStaticQuery } from "gatsby";
|
import Link from "next/link";
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
@@ -18,17 +18,7 @@ const StyledYearNext = styled(StyledYear)`
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function YearNavigation({ year }) {
|
export function YearNavigation({ year, yearList }) {
|
||||||
const yearList = useStaticQuery(graphql`
|
|
||||||
query {
|
|
||||||
allAnime {
|
|
||||||
groupedByYear: group(field: year) {
|
|
||||||
year: fieldValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`).allAnime.groupedByYear.map((group) => +group.year);
|
|
||||||
|
|
||||||
const previousYear = yearList.indexOf(year) > 0 ? yearList[yearList.indexOf(year) - 1] : null;
|
const previousYear = yearList.indexOf(year) > 0 ? yearList[yearList.indexOf(year) - 1] : null;
|
||||||
const nextYear = yearList.indexOf(year) < yearList.length - 1 ? yearList[yearList.indexOf(year) + 1] : null;
|
const nextYear = yearList.indexOf(year) < yearList.length - 1 ? yearList[yearList.indexOf(year) + 1] : null;
|
||||||
|
|
||||||
@@ -36,20 +26,20 @@ export function YearNavigation({ year }) {
|
|||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<StyledYearPrevious>
|
<StyledYearPrevious>
|
||||||
{previousYear && (
|
{previousYear && (
|
||||||
<Link to={`/year/${previousYear}`}>
|
<Link href={`/year/${previousYear}`} passHref>
|
||||||
<Button silent>{previousYear}</Button>
|
<Button as="a" silent>{previousYear}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</StyledYearPrevious>
|
</StyledYearPrevious>
|
||||||
<Link to={`/year`}>
|
<Link href={`/year`} passHref>
|
||||||
<Button silent>
|
<Button as="a" silent>
|
||||||
<Text variant="h1">{year}</Text>
|
<Text variant="h1">{year}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<StyledYearNext>
|
<StyledYearNext>
|
||||||
{nextYear && (
|
{nextYear && (
|
||||||
<Link to={`/year/${nextYear}`}>
|
<Link href={`/year/${nextYear}`} passHref>
|
||||||
<Button silent>{nextYear}</Button>
|
<Button as="a" silent>{nextYear}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</StyledYearNext>
|
</StyledYearNext>
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import { Listbox } from "components/listbox";
|
import { Listbox } from "components/listbox";
|
||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
import { graphql, useStaticQuery } from "gatsby";
|
|
||||||
|
|
||||||
export function SearchFilterYear({ value, setValue }) {
|
export function SearchFilterYear({ value, setValue }) {
|
||||||
const yearList = useStaticQuery(graphql`
|
const yearList = [ 2021, 2020, 2019 ];
|
||||||
query {
|
|
||||||
allAnime {
|
|
||||||
groupedByYear: group(field: year) {
|
|
||||||
year: fieldValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`).allAnime.groupedByYear
|
|
||||||
.map((node) => node.year)
|
|
||||||
.sort()
|
|
||||||
.reverse();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" alignItems="stretch" gapsColumn="0.5rem">
|
<Flex flexDirection="column" alignItems="stretch" gapsColumn="0.5rem">
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
} from "components/search-filter";
|
} from "components/search-filter";
|
||||||
import useEntitySearch from "hooks/useEntitySearch";
|
import useEntitySearch from "hooks/useEntitySearch";
|
||||||
import { SearchEntity } from "components/search";
|
import { SearchEntity } from "components/search";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import { AnimeSummaryCard } from "components/card";
|
import { AnimeSummaryCard } from "components/card";
|
||||||
|
import useSessionStorage from "hooks/useSessionStorage";
|
||||||
|
|
||||||
const sortByFields = new Map([
|
const sortByFields = new Map([
|
||||||
[ "A ➜ Z", "name" ],
|
[ "A ➜ Z", "name" ],
|
||||||
@@ -18,29 +18,23 @@ const sortByFields = new Map([
|
|||||||
]);
|
]);
|
||||||
const sortByOptions = [ ...sortByFields.keys() ];
|
const sortByOptions = [ ...sortByFields.keys() ];
|
||||||
|
|
||||||
export function SearchAnime({ searchQuery, locationState }) {
|
const initialFilter = {
|
||||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
firstLetter: null,
|
||||||
const filterSeason = locationState?.filterSeason || null;
|
season: null,
|
||||||
const filterYear = locationState?.filterYear || null;
|
year: null,
|
||||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
sortBy: sortByOptions[0]
|
||||||
|
};
|
||||||
|
|
||||||
const updateState = (field) => (newValue) => {
|
export function SearchAnime({ searchQuery }) {
|
||||||
navigate("", {
|
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||||
state: {
|
|
||||||
...locationState,
|
|
||||||
[field]: newValue
|
|
||||||
},
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitySearch = useEntitySearch("anime", searchQuery, {
|
const entitySearch = useEntitySearch("anime", searchQuery, {
|
||||||
filters: {
|
filters: {
|
||||||
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
"name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||||
season: filterSeason,
|
season: filter.season,
|
||||||
year: filterYear
|
year: filter.year
|
||||||
},
|
},
|
||||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,13 +42,13 @@ export function SearchAnime({ searchQuery, locationState }) {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||||
<SearchFilterSeason value={filterSeason} setValue={updateState("filterSeason")}/>
|
<SearchFilterSeason value={filter.season} setValue={updateFilter("season")}/>
|
||||||
<SearchFilterYear value={filterYear} setValue={updateState("filterYear")}/>
|
<SearchFilterYear value={filter.year} setValue={updateFilter("year")}/>
|
||||||
<SearchFilterSortBy
|
<SearchFilterSortBy
|
||||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||||
value={searchQuery ? "Relevance" : sortBy}
|
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||||
setValue={updateState("sortBy")}
|
setValue={updateFilter("sortBy")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||||
import useEntitySearch from "hooks/useEntitySearch";
|
import useEntitySearch from "hooks/useEntitySearch";
|
||||||
import { SearchEntity } from "components/search";
|
import { SearchEntity } from "components/search";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import { ArtistSummaryCard } from "components/card";
|
import { ArtistSummaryCard } from "components/card";
|
||||||
|
import useSessionStorage from "hooks/useSessionStorage";
|
||||||
|
|
||||||
const sortByFields = new Map([
|
const sortByFields = new Map([
|
||||||
[ "A ➜ Z", "name" ],
|
[ "A ➜ Z", "name" ],
|
||||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
|||||||
]);
|
]);
|
||||||
const sortByOptions = [ ...sortByFields.keys() ];
|
const sortByOptions = [ ...sortByFields.keys() ];
|
||||||
|
|
||||||
export function SearchArtist({ searchQuery, locationState }) {
|
const initialFilter = {
|
||||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
firstLetter: null,
|
||||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
sortBy: sortByOptions[0]
|
||||||
|
};
|
||||||
|
|
||||||
const updateState = (field) => (newValue) => {
|
export function SearchArtist({ searchQuery }) {
|
||||||
navigate("", {
|
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||||
state: {
|
|
||||||
...locationState,
|
|
||||||
[field]: newValue
|
|
||||||
},
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitySearch = useEntitySearch("artist", searchQuery, {
|
const entitySearch = useEntitySearch("artist", searchQuery, {
|
||||||
filters: {
|
filters: {
|
||||||
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
"name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||||
},
|
},
|
||||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,11 +31,11 @@ export function SearchArtist({ searchQuery, locationState }) {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||||
<SearchFilterSortBy
|
<SearchFilterSortBy
|
||||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||||
value={searchQuery ? "Relevance" : sortBy}
|
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||||
setValue={updateState("sortBy")}
|
setValue={updateFilter("sortBy")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { useLocation } from "@reach/router";
|
import Link from "next/link";
|
||||||
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
import { Box, Flex } from "components/box";
|
import { Box, Flex } from "components/box";
|
||||||
import { Link } from "gatsby";
|
|
||||||
import { Icon } from "components/icon";
|
import { Icon } from "components/icon";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { fetchGlobalSearchResults } from "lib/search";
|
import { fetchGlobalSearchResults } from "lib/client/search";
|
||||||
import { AnimeSummaryCard, ArtistSummaryCard, ErrorCard, SummaryCard, ThemeSummaryCard } from "components/card";
|
import { AnimeSummaryCard, ArtistSummaryCard, ErrorCard, SummaryCard, ThemeSummaryCard } from "components/card";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
export function SearchGlobal({ searchQuery }) {
|
export function SearchGlobal({ searchQuery }) {
|
||||||
const fetchSearchResults = () => fetchGlobalSearchResults(
|
const fetchSearchResults = () => fetchGlobalSearchResults(
|
||||||
searchQuery,
|
searchQuery,
|
||||||
4,
|
4,
|
||||||
[ "anime", "theme", "artist", "series", "studio" ]
|
["anime", "theme", "artist", "series", "studio"]
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -22,7 +22,7 @@ export function SearchGlobal({ searchQuery }) {
|
|||||||
isLoading,
|
isLoading,
|
||||||
isError
|
isError
|
||||||
} = useQuery(
|
} = useQuery(
|
||||||
[ "searchGlobal", searchQuery ],
|
["searchGlobal", searchQuery],
|
||||||
fetchSearchResults,
|
fetchSearchResults,
|
||||||
{
|
{
|
||||||
keepPreviousData: true
|
keepPreviousData: true
|
||||||
@@ -87,7 +87,8 @@ export function SearchGlobal({ searchQuery }) {
|
|||||||
title="Series"
|
title="Series"
|
||||||
results={seriesResults}
|
results={seriesResults}
|
||||||
renderSummaryCard={(series) => (
|
renderSummaryCard={(series) => (
|
||||||
<SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} />
|
<SummaryCard key={series.slug} title={series.name} description="Series"
|
||||||
|
to={`/series/${series.slug}`}/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<GlobalSearchSection
|
<GlobalSearchSection
|
||||||
@@ -95,7 +96,8 @@ export function SearchGlobal({ searchQuery }) {
|
|||||||
title="Studios"
|
title="Studios"
|
||||||
results={studioResults}
|
results={studioResults}
|
||||||
renderSummaryCard={(studio) => (
|
renderSummaryCard={(studio) => (
|
||||||
<SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} />
|
<SummaryCard key={studio.slug} title={studio.name} description="Studio"
|
||||||
|
to={`/studio/${studio.slug}`}/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -103,8 +105,8 @@ export function SearchGlobal({ searchQuery }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
|
function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
|
||||||
const { search, hash } = useLocation();
|
const router = useRouter();
|
||||||
const urlSuffix = search + hash;
|
const urlParams = router.query;
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
return null;
|
return null;
|
||||||
@@ -121,8 +123,8 @@ function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
|
|||||||
</Box>
|
</Box>
|
||||||
{hasMoreResults && (
|
{hasMoreResults && (
|
||||||
<Flex justifyContent="center">
|
<Flex justifyContent="center">
|
||||||
<Link to={`/search/${entity}${urlSuffix}`}>
|
<Link href={{ pathname: `/search/${entity}`, query: urlParams }} passHref>
|
||||||
<Button silent title="See all results">
|
<Button as="a" silent title="See all results">
|
||||||
<Icon icon={faChevronDown}/>
|
<Icon icon={faChevronDown}/>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||||
import useEntitySearch from "hooks/useEntitySearch";
|
import useEntitySearch from "hooks/useEntitySearch";
|
||||||
import { SearchEntity } from "components/search";
|
import { SearchEntity } from "components/search";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import { SummaryCard } from "components/card";
|
import { SummaryCard } from "components/card";
|
||||||
|
import useSessionStorage from "hooks/useSessionStorage";
|
||||||
|
|
||||||
const sortByFields = new Map([
|
const sortByFields = new Map([
|
||||||
[ "A ➜ Z", "name" ],
|
[ "A ➜ Z", "name" ],
|
||||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
|||||||
]);
|
]);
|
||||||
const sortByOptions = [ ...sortByFields.keys() ];
|
const sortByOptions = [ ...sortByFields.keys() ];
|
||||||
|
|
||||||
export function SearchSeries({ searchQuery, locationState }) {
|
const initialFilter = {
|
||||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
firstLetter: null,
|
||||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
sortBy: sortByOptions[0]
|
||||||
|
};
|
||||||
|
|
||||||
const updateState = (field) => (newValue) => {
|
export function SearchSeries({ searchQuery }) {
|
||||||
navigate("", {
|
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||||
state: {
|
|
||||||
...locationState,
|
|
||||||
[field]: newValue
|
|
||||||
},
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitySearch = useEntitySearch("series", searchQuery, {
|
const entitySearch = useEntitySearch("series", searchQuery, {
|
||||||
filters: {
|
filters: {
|
||||||
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
"name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||||
},
|
},
|
||||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,11 +31,11 @@ export function SearchSeries({ searchQuery, locationState }) {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||||
<SearchFilterSortBy
|
<SearchFilterSortBy
|
||||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||||
value={searchQuery ? "Relevance" : sortBy}
|
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||||
setValue={updateState("sortBy")}
|
setValue={updateFilter("sortBy")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||||
import useEntitySearch from "hooks/useEntitySearch";
|
import useEntitySearch from "hooks/useEntitySearch";
|
||||||
import { SearchEntity } from "components/search";
|
import { SearchEntity } from "components/search";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import { SummaryCard } from "components/card";
|
import { SummaryCard } from "components/card";
|
||||||
|
import useSessionStorage from "hooks/useSessionStorage";
|
||||||
|
|
||||||
const sortByFields = new Map([
|
const sortByFields = new Map([
|
||||||
[ "A ➜ Z", "name" ],
|
[ "A ➜ Z", "name" ],
|
||||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
|||||||
]);
|
]);
|
||||||
const sortByOptions = [ ...sortByFields.keys() ];
|
const sortByOptions = [ ...sortByFields.keys() ];
|
||||||
|
|
||||||
export function SearchStudio({ searchQuery, locationState }) {
|
const initialFilter = {
|
||||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
firstLetter: null,
|
||||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
sortBy: sortByOptions[0]
|
||||||
|
};
|
||||||
|
|
||||||
const updateState = (field) => (newValue) => {
|
export function SearchStudio({ searchQuery }) {
|
||||||
navigate("", {
|
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||||
state: {
|
|
||||||
...locationState,
|
|
||||||
[field]: newValue
|
|
||||||
},
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitySearch = useEntitySearch("studio", searchQuery, {
|
const entitySearch = useEntitySearch("studio", searchQuery, {
|
||||||
filters: {
|
filters: {
|
||||||
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
"name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||||
},
|
},
|
||||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,11 +31,11 @@ export function SearchStudio({ searchQuery, locationState }) {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||||
<SearchFilterSortBy
|
<SearchFilterSortBy
|
||||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||||
value={searchQuery ? "Relevance" : sortBy}
|
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||||
setValue={updateState("sortBy")}
|
setValue={updateFilter("sortBy")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter";
|
import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter";
|
||||||
import useEntitySearch from "hooks/useEntitySearch";
|
import useEntitySearch from "hooks/useEntitySearch";
|
||||||
import { SearchEntity } from "components/search";
|
import { SearchEntity } from "components/search";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import { ThemeSummaryCard } from "components/card";
|
import { ThemeSummaryCard } from "components/card";
|
||||||
|
import useSessionStorage from "hooks/useSessionStorage";
|
||||||
|
|
||||||
const sortByFields = new Map([
|
const sortByFields = new Map([
|
||||||
[ "A ➜ Z", "song.title" ],
|
[ "A ➜ Z", "song.title" ],
|
||||||
@@ -13,28 +13,22 @@ const sortByFields = new Map([
|
|||||||
]);
|
]);
|
||||||
const sortByOptions = [ ...sortByFields.keys() ];
|
const sortByOptions = [ ...sortByFields.keys() ];
|
||||||
|
|
||||||
export function SearchTheme({ searchQuery, locationState }) {
|
const initialFilter = {
|
||||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
firstLetter: null,
|
||||||
const filterType = locationState?.filterType || null;
|
type: null,
|
||||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
sortBy: sortByOptions[0]
|
||||||
|
};
|
||||||
|
|
||||||
const updateState = (field) => (newValue) => {
|
export function SearchTheme({ searchQuery }) {
|
||||||
navigate("", {
|
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||||
state: {
|
|
||||||
...locationState,
|
|
||||||
[field]: newValue
|
|
||||||
},
|
|
||||||
replace: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const entitySearch = useEntitySearch("theme", searchQuery, {
|
const entitySearch = useEntitySearch("theme", searchQuery, {
|
||||||
filters: {
|
filters: {
|
||||||
has: "song",
|
has: "song",
|
||||||
"song][title][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
"song][title][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||||
type: filterType
|
type: filter.type
|
||||||
},
|
},
|
||||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -42,12 +36,12 @@ export function SearchTheme({ searchQuery, locationState }) {
|
|||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
filters={
|
filters={
|
||||||
<>
|
<>
|
||||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||||
<SearchFilterThemeType value={filterType} setValue={updateState("filterType")}/>
|
<SearchFilterThemeType value={filter.type} setValue={updateFilter("type")}/>
|
||||||
<SearchFilterSortBy
|
<SearchFilterSortBy
|
||||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||||
value={searchQuery ? "Relevance" : sortBy}
|
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||||
setValue={updateState("sortBy")}
|
setValue={updateFilter("sortBy")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,23 @@
|
|||||||
import PropTypes from "prop-types";
|
import Head from "next/head";
|
||||||
import { useLocation } from "@reach/router";
|
import withBasePath from "utils/withBasePath";
|
||||||
import { Helmet } from "react-helmet";
|
|
||||||
import useSiteMeta from "hooks/useSiteMeta";
|
|
||||||
|
|
||||||
export function SEO({ title, description, meta = [], lang }) {
|
export function SEO({
|
||||||
const { pathname } = useLocation();
|
title,
|
||||||
|
description = "AnimeThemes is a simple and consistent repository of anime opening and ending themes.",
|
||||||
const {
|
image = withBasePath("/img/logo.svg"),
|
||||||
description: defaultDescription,
|
children
|
||||||
lang: defaultLanguage,
|
}) {
|
||||||
titleTemplate,
|
const titleWithSuffix = title ? `${title} · AnimeThemes` : "AnimeThemes";
|
||||||
siteName,
|
|
||||||
siteUrl
|
|
||||||
} = useSiteMeta();
|
|
||||||
|
|
||||||
const seo = {
|
|
||||||
url: `${siteUrl}${pathname}`,
|
|
||||||
lang: lang || defaultLanguage,
|
|
||||||
title: title || siteName,
|
|
||||||
description: description || defaultDescription
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Helmet
|
<Head>
|
||||||
htmlAttributes={{
|
<title>{titleWithSuffix}</title>
|
||||||
lang: seo.lang
|
<meta property="description" content={description}/>
|
||||||
}}
|
<meta property="og:title" content={titleWithSuffix}/>
|
||||||
|
<meta property="og:description" content={description}/>
|
||||||
title={seo.title}
|
<meta property="og:image" content={image}/>
|
||||||
titleTemplate={title ? titleTemplate : null}
|
<meta property="og:site_name" content="AnimeThemes"/>
|
||||||
meta={[
|
{children}
|
||||||
{
|
</Head>
|
||||||
name: "description",
|
);
|
||||||
content: seo.description
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: "og:site_name",
|
|
||||||
content: siteName
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: "og:title",
|
|
||||||
content: title ? titleTemplate.replace(/%s/g, title) : seo.title
|
|
||||||
},
|
|
||||||
{
|
|
||||||
property: "og:description",
|
|
||||||
content: seo.description
|
|
||||||
}
|
|
||||||
].concat(meta)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SEO.propTypes = {
|
|
||||||
title: PropTypes.string,
|
|
||||||
description: PropTypes.string,
|
|
||||||
meta: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
lang: PropTypes.string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { AnimateSharedLayout, motion } from "framer-motion";
|
import { AnimateSharedLayout, m } from "framer-motion";
|
||||||
import { Box } from "components/box";
|
import { Box } from "components/box";
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import theme from "theme";
|
import theme from "theme";
|
||||||
@@ -26,7 +26,7 @@ const StyledButton = styled(Button)`
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const StyledButtonBackground = styled(motion.div)`
|
const StyledButtonBackground = styled(m.div)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: ${theme.zIndices.switcherButton};
|
z-index: ${theme.zIndices.switcherButton};
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { motion } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
|
|
||||||
export function Collapse({ collapse, children }) {
|
export function Collapse({ collapse, children }) {
|
||||||
if (collapse) {
|
if (collapse) {
|
||||||
@@ -6,12 +6,12 @@ export function Collapse({ collapse, children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<m.div
|
||||||
initial={{ y: -20, opacity: 0 }}
|
initial={{ y: -20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ type: "tween", duration: 0.35, ease: [ 0, 0.66, 0.46, 0.98 ] }}
|
transition={{ type: "tween", duration: 0.35, ease: [ 0, 0.66, 0.46, 0.98 ] }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</m.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Link } from "gatsby";
|
import Link from "next/link";
|
||||||
import { Text } from "components/text";
|
import { Text } from "components/text";
|
||||||
|
|
||||||
// Specify an artist if you want to display this in an artist context (e.g. artist page)
|
// Specify an artist if you want to display this in an artist context (e.g. artist page)
|
||||||
@@ -10,8 +10,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
|||||||
<Text>
|
<Text>
|
||||||
{songTitleLinkTo
|
{songTitleLinkTo
|
||||||
? (
|
? (
|
||||||
<Link to={songTitleLinkTo}>
|
<Link href={songTitleLinkTo} passHref>
|
||||||
<Text link italics={!song.title}>{songTitle}</Text>
|
<Text as="a" link italics={!song.title}>{songTitle}</Text>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
@@ -27,8 +27,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
|||||||
{!!performedAs.as && (
|
{!!performedAs.as && (
|
||||||
<>
|
<>
|
||||||
<Text variant="small" color="text-muted"> as </Text>
|
<Text variant="small" color="text-muted"> as </Text>
|
||||||
<Link to={`/artist/${performedAs.artist.slug}`}>
|
<Link href={`/artist/${performedAs.artist.slug}`} passHref>
|
||||||
<Text link>
|
<Text as="a" link>
|
||||||
{performedAs.as}
|
{performedAs.as}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -39,8 +39,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
|||||||
<Text variant="small" color="text-muted"> with </Text>
|
<Text variant="small" color="text-muted"> with </Text>
|
||||||
{performedWith.map((performance, index) => (
|
{performedWith.map((performance, index) => (
|
||||||
<Fragment key={performance.artist.slug}>
|
<Fragment key={performance.artist.slug}>
|
||||||
<Link to={`/artist/${performance.artist.slug}`}>
|
<Link href={`/artist/${performance.artist.slug}`} passHref>
|
||||||
<Text link>
|
<Text as="a" link>
|
||||||
{performance.as || performance.artist.name}
|
{performance.as || performance.artist.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -60,8 +60,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
|||||||
<Text variant="small" color="text-muted"> by </Text>
|
<Text variant="small" color="text-muted"> by </Text>
|
||||||
{song.performances.map((performance, index) => (
|
{song.performances.map((performance, index) => (
|
||||||
<Fragment key={performance.artist.slug}>
|
<Fragment key={performance.artist.slug}>
|
||||||
<Link to={`/artist/${performance.artist.slug}`}>
|
<Link href={`/artist/${performance.artist.slug}`} passHref>
|
||||||
<Text link>
|
<Text as="a" link>
|
||||||
{performance.as || performance.artist.name}
|
{performance.as || performance.artist.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export { AspectRatio } from "./AspectRatio";
|
export { AspectRatio } from "./AspectRatio";
|
||||||
export { Collapse } from "./Collapse";
|
export { Collapse } from "./Collapse";
|
||||||
export { HeightTransition } from "./HeightTransition";
|
export { HeightTransition } from "./HeightTransition";
|
||||||
export { SongTitleWithArtists } from "./SongTitleWithArtists"
|
export { SongTitleWithArtists } from "./SongTitleWithArtists";
|
||||||
export { ThemeEntryTags } from "./ThemeEntryTags";
|
export { ThemeEntryTags } from "./ThemeEntryTags";
|
||||||
export { VideoTags } from "./VideoTags";
|
export { VideoTags } from "./VideoTags";
|
||||||
export { HorizontalScroll } from "./HorizontalScroll";
|
export { HorizontalScroll } from "./HorizontalScroll";
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
StyledOverlay, StyledPlayerInfo, StyledPlayerProgress, StyledPlayerProgressBar
|
StyledOverlay, StyledPlayerInfo, StyledPlayerProgress, StyledPlayerProgressBar
|
||||||
} from "./VideoPlayer.style";
|
} from "./VideoPlayer.style";
|
||||||
import { faDownload, faExpandAlt, faPause, faPlay, faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faDownload, faExpandAlt, faPause, faPlay, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { navigate } from "gatsby";
|
|
||||||
import PlayerContext from "context/playerContext";
|
import PlayerContext from "context/playerContext";
|
||||||
import createVideoSlug from "utils/createVideoSlug";
|
import createVideoSlug from "utils/createVideoSlug";
|
||||||
import { useMedia } from "use-media";
|
import { useMedia } from "use-media";
|
||||||
@@ -17,8 +16,9 @@ import { Text } from "components/text";
|
|||||||
import { Flex } from "components/box";
|
import { Flex } from "components/box";
|
||||||
import { Container } from "components/container";
|
import { Container } from "components/container";
|
||||||
import useCompatability from "hooks/useCompatability";
|
import useCompatability from "hooks/useCompatability";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const videoBaseUrl = process.env.GATSBY_VIDEO_URL || "https://animethemes.moe";
|
const videoBaseUrl = process.env.NEXT_PUBLIC_VIDEO_URL || "https://animethemes.moe";
|
||||||
|
|
||||||
export function VideoPlayer({ video, entry, background, ...props }) {
|
export function VideoPlayer({ video, entry, background, ...props }) {
|
||||||
const [isPlaying, setPlaying] = useState(false);
|
const [isPlaying, setPlaying] = useState(false);
|
||||||
@@ -28,6 +28,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
|
|||||||
const { setCurrentVideo } = useContext(PlayerContext);
|
const { setCurrentVideo } = useContext(PlayerContext);
|
||||||
const isMobile = useMedia({ maxWidth: "720px" });
|
const isMobile = useMedia({ maxWidth: "720px" });
|
||||||
const videoUrl = `${videoBaseUrl}/video/${video.basename}`;
|
const videoUrl = `${videoBaseUrl}/video/${video.basename}`;
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@@ -39,7 +40,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
|
|||||||
|
|
||||||
function maximize() {
|
function maximize() {
|
||||||
const videoSlug = createVideoSlug(entry.theme, entry, video);
|
const videoSlug = createVideoSlug(entry.theme, entry, video);
|
||||||
navigate(`/anime/${entry.theme.anime.slug}/${videoSlug}`);
|
router.push(`/anime/${entry.theme.anime.slug}/${videoSlug}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preventTextSelection(event) {
|
function preventTextSelection(event) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import styled, { css, keyframes } from "styled-components";
|
import styled, { css, keyframes } from "styled-components";
|
||||||
import { motion } from "framer-motion";
|
import { m } from "framer-motion";
|
||||||
import { gapsColumn, gapsRow } from "styles/mixins";
|
import { gapsColumn, gapsRow } from "styles/mixins";
|
||||||
import { Button } from "components/button";
|
import { Button } from "components/button";
|
||||||
import theme from "theme";
|
import theme from "theme";
|
||||||
@@ -14,7 +14,7 @@ const slideIn = keyframes`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledPlayer = styled(motion.div)`
|
export const StyledPlayer = styled(m.div)`
|
||||||
${(props) => props.background ? css`
|
${(props) => props.background ? css`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: ${theme.zIndices.videoPlayer};
|
z-index: ${theme.zIndices.videoPlayer};
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
import { graphql, useStaticQuery } from "gatsby";
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchCurrentSeason } from "lib/client/currentSeason";
|
||||||
|
|
||||||
const seasonOrder = [
|
let cache = null;
|
||||||
"winter",
|
|
||||||
"spring",
|
|
||||||
"summer",
|
|
||||||
"fall"
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function useCurrentSeason() {
|
export default function useCurrentSeason() {
|
||||||
const { currentSeasonAnime } = useStaticQuery(graphql`
|
const [ current, setCurrent ] = useState(null);
|
||||||
query {
|
|
||||||
currentSeasonAnime: allAnime {
|
useEffect(() => {
|
||||||
groupedByYear: group(field: year) {
|
if (!cache) {
|
||||||
year: fieldValue
|
fetchCurrentSeason()
|
||||||
nodes {
|
.then((current) => {
|
||||||
season
|
cache = current;
|
||||||
}
|
|
||||||
}
|
setCurrent(current);
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
setCurrent(cache);
|
||||||
}
|
}
|
||||||
`);
|
}, []);
|
||||||
|
|
||||||
const currentYearGroup = currentSeasonAnime.groupedByYear.sort((a, b) => b.year - a.year)[0];
|
return {
|
||||||
const currentYear = currentYearGroup.year;
|
currentYear: current?.year,
|
||||||
const currentSeason = seasonOrder[currentYearGroup.nodes.map(({ season }) => seasonOrder.indexOf(season.toLowerCase())).sort().pop()];
|
currentSeason: current?.season.toLowerCase()
|
||||||
|
};
|
||||||
return { currentYear, currentSeason };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fetchEntitySearchResults } from "lib/search";
|
import { fetchEntitySearchResults } from "lib/client/search";
|
||||||
import { useInfiniteQuery, useQueryClient } from "react-query";
|
import { useInfiniteQuery, useQueryClient } from "react-query";
|
||||||
|
|
||||||
export default function useEntitySearch(entity, query, params = {}) {
|
export default function useEntitySearch(entity, query, params = {}) {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export default function useImage(resourceWithImages) {
|
|||||||
for (const image of resourceWithImages.images) {
|
for (const image of resourceWithImages.images) {
|
||||||
switch (image.facet) {
|
switch (image.facet) {
|
||||||
case "Small Cover":
|
case "Small Cover":
|
||||||
images.smallCover = image.link;
|
images.smallCover = image.link.replace("localhost", "staging.animethemes.moe");
|
||||||
break;
|
break;
|
||||||
case "Large Cover":
|
case "Large Cover":
|
||||||
images.largeCover = image.link;
|
images.largeCover = image.link.replace("localhost", "staging.animethemes.moe");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Ignore
|
// Ignore
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function useLocation() {
|
||||||
|
const [ location, setLocation ] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => setLocation(window.location), []);
|
||||||
|
|
||||||
|
return location;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionStorage(sessionStorage);
|
||||||
|
setData((oldData) => ({ ...oldData, ...loadedData }));
|
||||||
|
}, [ key, initialData ]);
|
||||||
|
|
||||||
|
const updateDataField = (field) => (newValue) => {
|
||||||
|
if (newValue === initialData[field]) {
|
||||||
|
sessionStorage?.removeItem(createKey(key, field));
|
||||||
|
} else {
|
||||||
|
sessionStorage?.setItem(createKey(key, field), newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData((oldData) => ({
|
||||||
|
...oldData,
|
||||||
|
[field]: newValue
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
updateDataField
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useStaticQuery, graphql } from "gatsby";
|
|
||||||
|
|
||||||
export default function useSiteMeta() {
|
|
||||||
const { site } = useStaticQuery(query);
|
|
||||||
return site.siteMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const query = graphql`
|
|
||||||
query SEO {
|
|
||||||
site {
|
|
||||||
siteMetadata {
|
|
||||||
titleTemplate
|
|
||||||
description
|
|
||||||
siteName
|
|
||||||
siteUrl
|
|
||||||
lang
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,76 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { ThemeProvider } from "styled-components";
|
|
||||||
import { Helmet } from "react-helmet";
|
|
||||||
import GlobalStyle from "styles/global";
|
|
||||||
import theme from "theme";
|
|
||||||
import { Navigation, SearchNavigation, SeasonNavigation, YearNavigation } from "components/navigation";
|
|
||||||
import { Container } from "components/container";
|
|
||||||
import { VideoPlayer } from "components/video-player";
|
|
||||||
import PlayerContext from "context/playerContext";
|
|
||||||
import { SEO } from "components/seo";
|
|
||||||
import ColorThemeContext from "context/colorThemeContext";
|
|
||||||
import useColorTheme from "hooks/useColorTheme";
|
|
||||||
import { Box, Flex } from "components/box";
|
|
||||||
import { darkColors } from "theme/colors/dark";
|
|
||||||
import { Footer } from "components/footer";
|
|
||||||
import { QueryClient, QueryClientProvider } from "react-query";
|
|
||||||
import "styles/fonts.scss";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
export default function Layout({ children, data, pageContext, location }) {
|
|
||||||
const video = data ? data.video : null;
|
|
||||||
const entry = data ? data.entry : null;
|
|
||||||
|
|
||||||
const [ currentVideo, setCurrentVideo ] = useState(video);
|
|
||||||
const [ currentEntry, setCurrentEntry ] = useState(entry);
|
|
||||||
const [ colorTheme, toggleColorTheme ] = useColorTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (video && entry) {
|
|
||||||
setCurrentVideo(video);
|
|
||||||
setCurrentEntry(entry);
|
|
||||||
}
|
|
||||||
}, [ video, entry ]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<ColorThemeContext.Provider value={{ colorTheme, toggleColorTheme }}>
|
|
||||||
<PlayerContext.Provider value={{ currentVideo, setCurrentVideo }}>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<SEO />
|
|
||||||
<Helmet>
|
|
||||||
<meta name="theme-color" content={darkColors["background"]}/>
|
|
||||||
</Helmet>
|
|
||||||
<GlobalStyle/>
|
|
||||||
<Flex flexDirection="column" minHeight="100%" bg="background">
|
|
||||||
<Navigation offsetToggleButton={!!currentVideo && !video}/>
|
|
||||||
{currentVideo && (
|
|
||||||
<VideoPlayer
|
|
||||||
video={currentVideo}
|
|
||||||
entry={currentEntry}
|
|
||||||
background={!video}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Container mb="2rem">
|
|
||||||
{!!pageContext.year && (
|
|
||||||
<Box gapsColumn="1rem" mb="1.5rem">
|
|
||||||
<YearNavigation year={pageContext.year} />
|
|
||||||
<SeasonNavigation year={pageContext.year} season={pageContext.season} seasonList={pageContext.seasonList} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{"entity" in pageContext && (
|
|
||||||
<Box mb="1.5rem">
|
|
||||||
<SearchNavigation entity={pageContext.entity} location={location} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</Container>
|
|
||||||
<Footer/>
|
|
||||||
</Flex>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</PlayerContext.Provider>
|
|
||||||
</ColorThemeContext.Provider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const baseUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { baseUrl } from "lib/client/api";
|
||||||
|
|
||||||
|
export async function fetchCurrentSeason() {
|
||||||
|
const res = await fetch(`${baseUrl}/api/anime?sort=-year,-season&page[size]=1&fields[anime]=year,season`);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
return json.anime[0];
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { baseUrl } from "gatsby-source-animethemes/src";
|
import { baseUrl } from "lib/client/api";
|
||||||
|
|
||||||
const backLog = [];
|
const backLog = [];
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { baseUrl } from "gatsby-source-animethemes/src";
|
import { baseUrl } from "lib/client/api";
|
||||||
|
|
||||||
const entityConfigs = {
|
const entityConfigs = {
|
||||||
anime: {
|
anime: {
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const mappings = require("./mappings.json");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Query: {
|
||||||
|
bracket: async (_, { slug }) => {
|
||||||
|
const bracket = await fetchJson(`https://animebracket.com/api/bracket/${slug}`);
|
||||||
|
const rounds = await fetchJson(`https://animebracket.com/api/results/${slug}`);
|
||||||
|
const currentRound = await fetchJson(`https://animebracket.com/api/rounds/${slug}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
name: bracket.name,
|
||||||
|
currentRound,
|
||||||
|
currentGroup: currentRound,
|
||||||
|
rounds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bracket: {
|
||||||
|
currentRound: (bracket) => {
|
||||||
|
if (bracket.currentRound) {
|
||||||
|
return {
|
||||||
|
tier: +bracket.currentRound[0].tier,
|
||||||
|
pairings: bracket.currentRound
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
currentGroup: (bracket) => {
|
||||||
|
if (bracket.currentRound) {
|
||||||
|
return bracket.currentRound[0].group;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
rounds: (bracket) => {
|
||||||
|
return bracket.rounds.map((round) => ({
|
||||||
|
tier: round[0].tier,
|
||||||
|
pairings: round
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BracketRound: {
|
||||||
|
name: (round) => {
|
||||||
|
switch (round.pairings.length) {
|
||||||
|
case 1:
|
||||||
|
return "Finals";
|
||||||
|
case 2:
|
||||||
|
return "Semifinals";
|
||||||
|
case 4:
|
||||||
|
return "Quarterfinals";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pairings: (round) => {
|
||||||
|
return round.pairings
|
||||||
|
.filter((pairing) => !pairing.filler)
|
||||||
|
.map((pairing) => ({
|
||||||
|
order: +pairing.order,
|
||||||
|
group: +pairing.group,
|
||||||
|
characterA: pairing.character1,
|
||||||
|
characterB: pairing.character2,
|
||||||
|
votesA: pairing.character1.votes || null,
|
||||||
|
votesB: pairing.character2.votes || null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BracketPairing: {
|
||||||
|
characterA: bracketCharacterResolver("characterA"),
|
||||||
|
characterB: bracketCharacterResolver("characterB")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function bracketCharacterResolver(field) {
|
||||||
|
return (pairing) => {
|
||||||
|
const character = pairing[field];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: character.id,
|
||||||
|
seed: character.seed,
|
||||||
|
name: character.name,
|
||||||
|
source: character.source,
|
||||||
|
theme: mappings[character.id]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
module.exports = `
|
||||||
|
#graphql
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
bracket(slug: String): Bracket
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bracket {
|
||||||
|
slug: String!
|
||||||
|
name: String!
|
||||||
|
currentRound: BracketRound
|
||||||
|
currentGroup: Int
|
||||||
|
rounds: [BracketRound]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BracketRound {
|
||||||
|
bracket: Bracket
|
||||||
|
tier: Int!
|
||||||
|
name: String
|
||||||
|
pairings: [BracketPairing]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BracketPairing {
|
||||||
|
round: BracketRound
|
||||||
|
order: Int!
|
||||||
|
group: Int!
|
||||||
|
characterA: BracketCharacter
|
||||||
|
characterB: BracketCharacter
|
||||||
|
votesA: Int
|
||||||
|
votesB: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type BracketCharacter {
|
||||||
|
pairing: BracketPairing
|
||||||
|
id: Int!
|
||||||
|
seed: Int!
|
||||||
|
name: String!
|
||||||
|
source: String!
|
||||||
|
theme: Theme
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
const { videoSource, imageFacet, animeSeason, resourceSite, themeType, videoOverlap } = require("lib/server/animethemes/enums");
|
||||||
|
|
||||||
|
const knex = require("knex")({
|
||||||
|
client: "mysql2",
|
||||||
|
connection: {
|
||||||
|
host: process.env.DB_HOST || "127.0.0.1",
|
||||||
|
port: process.env.DB_PORT || 3306,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE || "animethemes"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Query: {
|
||||||
|
anime: (_, { id, slug }) => knex("anime")
|
||||||
|
.where((builder) => {
|
||||||
|
if (id) {
|
||||||
|
builder.where("anime_id", id);
|
||||||
|
}
|
||||||
|
if (slug) {
|
||||||
|
builder.where("slug", slug);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
animeAll: (_, { limit, year, season }) => {
|
||||||
|
const query = knex("anime");
|
||||||
|
|
||||||
|
if (year) {
|
||||||
|
query.where("year", year);
|
||||||
|
}
|
||||||
|
if (season) {
|
||||||
|
query.where(
|
||||||
|
"season",
|
||||||
|
[ ...animeSeason.values() ]
|
||||||
|
.findIndex((seasonName) => seasonName.toLowerCase() === season.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.select();
|
||||||
|
},
|
||||||
|
theme: (_, { id }) => knex("anime_themes")
|
||||||
|
.where((builder) => {
|
||||||
|
if (id) {
|
||||||
|
builder.where("theme_id", id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
themeAll: (_, { limit, orderBy, orderDesc }) => {
|
||||||
|
const query = knex("anime_themes");
|
||||||
|
|
||||||
|
if (orderBy) {
|
||||||
|
query.orderBy(orderBy, orderDesc ? "desc" : "asc");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.select();
|
||||||
|
},
|
||||||
|
artist: (_, { id, slug }) => knex("artists")
|
||||||
|
.where((builder) => {
|
||||||
|
if (id) {
|
||||||
|
builder.where("artist_id", id);
|
||||||
|
}
|
||||||
|
if (slug) {
|
||||||
|
builder.where("slug", slug);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
artistAll: (_, { limit }) => {
|
||||||
|
const query = knex("artists");
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.select();
|
||||||
|
},
|
||||||
|
series: (_, { id, slug }) => knex("series")
|
||||||
|
.where((builder) => {
|
||||||
|
if (id) {
|
||||||
|
builder.where("series_id", id);
|
||||||
|
}
|
||||||
|
if (slug) {
|
||||||
|
builder.where("slug", slug);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
seriesAll: (_, { limit }) => {
|
||||||
|
const query = knex("series");
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.select();
|
||||||
|
},
|
||||||
|
studio: (_, { id, slug }) => knex("studios")
|
||||||
|
.where((builder) => {
|
||||||
|
if (id) {
|
||||||
|
builder.where("studio_id", id);
|
||||||
|
}
|
||||||
|
if (slug) {
|
||||||
|
builder.where("slug", slug);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first(),
|
||||||
|
studioAll: (_, { limit }) => {
|
||||||
|
const query = knex("studios");
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.select();
|
||||||
|
},
|
||||||
|
year: (_, { value }) => ({ value }),
|
||||||
|
yearAll: () => knex("anime")
|
||||||
|
.groupBy("year")
|
||||||
|
.select("year")
|
||||||
|
.then((results) => results.map((anime) => ({ value: anime.year }))),
|
||||||
|
season: (_, { value, year }) => ({
|
||||||
|
value: [ ...animeSeason.values() ]
|
||||||
|
.findIndex((seasonName) => seasonName.toLowerCase() === value.toLowerCase()),
|
||||||
|
year: { value: year }
|
||||||
|
}),
|
||||||
|
seasonAll: (_, { year }) => knex("anime")
|
||||||
|
.where((builder) => {
|
||||||
|
if (year) {
|
||||||
|
builder.where("year", year);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.groupBy([ "season", "year" ])
|
||||||
|
.select([ "season", "year" ])
|
||||||
|
.then((results) => results.map((anime) => ({ value: anime.season, year: { value: year } }))),
|
||||||
|
},
|
||||||
|
Year: {
|
||||||
|
seasons: (year) => knex("anime")
|
||||||
|
.where("year", year.value)
|
||||||
|
.groupBy("season")
|
||||||
|
.select("season")
|
||||||
|
.then((results) => results.map((anime) => ({ value: anime.season, year }))),
|
||||||
|
},
|
||||||
|
Season: {
|
||||||
|
value: (season) => animeSeason.get(season.value),
|
||||||
|
anime: (season) => knex("anime")
|
||||||
|
.where((builder) => {
|
||||||
|
builder.where("season", season.value);
|
||||||
|
|
||||||
|
if (season.year) {
|
||||||
|
builder.where("year", season.year.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(),
|
||||||
|
},
|
||||||
|
Anime: {
|
||||||
|
id: (anime) => anime.anime_id,
|
||||||
|
season: (anime) => animeSeason.get(anime.season),
|
||||||
|
synonyms: (anime) => knex("anime_synonyms").where({ anime_id: anime.anime_id }).select(),
|
||||||
|
themes: (anime) => knex("anime_themes").where({ anime_id: anime.anime_id }).select(),
|
||||||
|
series: (anime) => knex("series")
|
||||||
|
.innerJoin("anime_series", "anime_series.series_id", "series.series_id")
|
||||||
|
.where({ "anime_series.anime_id": anime.anime_id })
|
||||||
|
.select("series.*"),
|
||||||
|
studios: (anime) => knex("studios")
|
||||||
|
.innerJoin("anime_studio", "anime_studio.studio_id", "studios.studio_id")
|
||||||
|
.where({ "anime_studio.anime_id": anime.anime_id })
|
||||||
|
.select("studios.*"),
|
||||||
|
resources: (anime) => knex("resources")
|
||||||
|
.innerJoin("anime_resource", "anime_resource.resource_id", "resources.resource_id")
|
||||||
|
.where({ "anime_resource.anime_id": anime.anime_id })
|
||||||
|
.select("resources.*"),
|
||||||
|
images: (anime) => knex("images")
|
||||||
|
.innerJoin("anime_image", "anime_image.image_id", "images.image_id")
|
||||||
|
.where({ "anime_image.anime_id": anime.anime_id })
|
||||||
|
.select("images.*")
|
||||||
|
},
|
||||||
|
Theme: {
|
||||||
|
type: (theme) => themeType.get(theme.type),
|
||||||
|
sequence: (theme) => theme.sequence || 0,
|
||||||
|
song: (theme) => knex("songs").where({ song_id: theme.song_id }).first(),
|
||||||
|
anime: (theme) => knex("anime").where({ anime_id: theme.anime_id }).first(),
|
||||||
|
entries: (theme) => knex("anime_theme_entries").where({ theme_id: theme.theme_id }).select()
|
||||||
|
},
|
||||||
|
Artist: {
|
||||||
|
performances: (artist) => knex("artist_song").where({ artist_id: artist.artist_id }).select(),
|
||||||
|
images: (artist) => knex("images")
|
||||||
|
.innerJoin("artist_image", "artist_image.image_id", "images.image_id")
|
||||||
|
.where({ "artist_image.artist_id": artist.artist_id })
|
||||||
|
.select("images.*")
|
||||||
|
},
|
||||||
|
Song: {
|
||||||
|
themes: (song) => knex("anime_themes").where({ song_id: song.song_id }).select(),
|
||||||
|
performances: (song) => knex("artist_song").where({ song_id: song.song_id }).select(),
|
||||||
|
},
|
||||||
|
Performance: {
|
||||||
|
artist: (performance) => knex("artists").where({ artist_id: performance.artist_id }).first(),
|
||||||
|
song: (performance) => knex("songs").where({ song_id: performance.song_id }).first()
|
||||||
|
},
|
||||||
|
Entry: {
|
||||||
|
version: (entry) => entry.version || 1,
|
||||||
|
videos: (entry) => knex("videos")
|
||||||
|
.innerJoin("anime_theme_entry_video", "anime_theme_entry_video.video_id", "videos.video_id")
|
||||||
|
.where({ "anime_theme_entry_video.entry_id": entry.entry_id })
|
||||||
|
.select("videos.*")
|
||||||
|
},
|
||||||
|
Video: {
|
||||||
|
source: (video) => videoSource.get(video.source),
|
||||||
|
overlap: (video) => videoOverlap.get(video.overlap),
|
||||||
|
tags: (video) => {
|
||||||
|
const tags = [];
|
||||||
|
|
||||||
|
if (video.nc) {
|
||||||
|
tags.push("NC");
|
||||||
|
}
|
||||||
|
if (video.source === 2 || video.source === 3) {
|
||||||
|
tags.push(videoSource.get(video.source));
|
||||||
|
}
|
||||||
|
if (video.resolution && video.resolution !== 720) {
|
||||||
|
tags.push(video.resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (video.subbed) {
|
||||||
|
tags.push("Subbed");
|
||||||
|
} else if (video.lyrics) {
|
||||||
|
tags.push("Lyrics");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags.join("");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Image: {
|
||||||
|
facet: (image) => imageFacet.get(image.facet),
|
||||||
|
link: (image) => `https://staging.animethemes.moe/image/${image.image_id}`
|
||||||
|
},
|
||||||
|
Resource: {
|
||||||
|
site: (resource) => resourceSite.get(resource.site)
|
||||||
|
},
|
||||||
|
Series: {
|
||||||
|
anime: (series) => knex("anime")
|
||||||
|
.innerJoin("anime_series", "anime_series.anime_id", "anime.anime_id")
|
||||||
|
.where({ "anime_series.series_id": series.series_id })
|
||||||
|
.select("anime.*")
|
||||||
|
},
|
||||||
|
Studio: {
|
||||||
|
anime: (studio) => knex("anime")
|
||||||
|
.innerJoin("anime_studio", "anime_studio.anime_id", "anime.anime_id")
|
||||||
|
.where({ "anime_studio.studio_id": studio.studio_id })
|
||||||
|
.select("anime.*"),
|
||||||
|
resources: (studio) => knex("resources")
|
||||||
|
.innerJoin("studio_resource", "studio_resource.resource_id", "resources.resource_id")
|
||||||
|
.where({ "studio_resource.studio_id": studio.studio_id })
|
||||||
|
.select("resources.*")
|
||||||
|
},
|
||||||
|
BracketCharacter: {
|
||||||
|
theme: (character) => knex("anime_themes")
|
||||||
|
.where("theme_id", character.theme)
|
||||||
|
.first()
|
||||||
|
}
|
||||||
|
};
|
||||||