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": {
|
||||
"__PATH_PREFIX__": true
|
||||
},
|
||||
"extends": [
|
||||
"react-app",
|
||||
"plugin:react/recommended"
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"error",
|
||||
"all"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"components/*/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/self-closing-comp": "error",
|
||||
// For now we don't habe prop-types validation
|
||||
"react/prop-types": "off"
|
||||
}
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"error",
|
||||
"all"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
4,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"no-duplicate-imports": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"patterns": [
|
||||
"components/*/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/self-closing-comp": "error",
|
||||
// For now we don't habe prop-types validation
|
||||
"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
|
||||
.env
|
||||
.env.*
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# NPM
|
||||
node_modules
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# Gatsby
|
||||
/.cache
|
||||
/public
|
||||
/schema.json
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# 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)
|
||||
*.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
|
||||
|
||||
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
|
||||
|
||||
1. `npm install`
|
||||
2. `npm run develop`
|
||||
2. `npm run dev`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Hosting on root path
|
||||
|
||||
1. `npm run build`
|
||||
2. Upload the contents of `public` to your server.
|
||||
|
||||
### 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.
|
||||
1. `npm install` (If not done already.)
|
||||
2. `npm run build` generates all static pages and produces a production ready bundle.
|
||||
3. `npm run start` starts the server.
|
||||
4. The server is now listening on port 3000 for incoming requests.
|
||||
|
||||
## Used technologies
|
||||
|
||||
- [Gatsby](https://www.gatsbyjs.com/)
|
||||
- [Next.js](https://www.nextjs.org/)
|
||||
- [styled-components](https://styled-components.com/)
|
||||
- [react-query](https://react-query.tanstack.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",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"develop": "gatsby develop",
|
||||
"build": "gatsby build",
|
||||
"build-with-paths": "gatsby build --prefix-paths"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/nunito": "^4.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@reach/listbox": "^0.16.1",
|
||||
"@react-hook/resize-observer": "^1.2.2",
|
||||
"babel-plugin-prismjs": "^2.0.1",
|
||||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"babel-preset-gatsby": "^1.3.0",
|
||||
"common-tags": "^1.8.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"esm": "^3.2.25",
|
||||
"framer-motion": "^4.1.3",
|
||||
"gatsby": "^3.13.0",
|
||||
"gatsby-plugin-fontawesome-css": "^1.1.0",
|
||||
"gatsby-plugin-layout": "^2.3.0",
|
||||
"gatsby-plugin-manifest": "^3.3.0",
|
||||
"gatsby-plugin-react-helmet": "^4.3.0",
|
||||
"gatsby-plugin-root-import": "^2.0.6",
|
||||
"gatsby-plugin-sass": "^4.3.0",
|
||||
"gatsby-plugin-styled-components": "^4.3.0",
|
||||
"gatsby-source-animethemes": "file:plugins/gatsby-source-animethemes",
|
||||
"gatsby-source-animethemes-db": "file:plugins/gatsby-source-animethemes-db",
|
||||
"luxon": "^2.0.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prismjs": "^1.24.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"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"
|
||||
}
|
||||
"name": "animethemes-web",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@graphql-tools/merge": "^8.2.1",
|
||||
"@graphql-tools/schema": "^8.2.0",
|
||||
"@next/bundle-analyzer": "^12.0.3",
|
||||
"@reach/listbox": "^0.16.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"framer-motion": "^4.1.17",
|
||||
"graphql": "^15.6.1",
|
||||
"knex": "^0.95.11",
|
||||
"lodash": "^4.17.21",
|
||||
"mysql2": "^2.3.3",
|
||||
"next": "^12.0.5-canary.6",
|
||||
"prismjs": "^1.25.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-query": "^3.29.0",
|
||||
"sass": "^1.43.4",
|
||||
"styled-components": "^5.3.3",
|
||||
"styled-system": "^5.1.5",
|
||||
"use-debounce": "^7.0.0",
|
||||
"use-media": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-next": "^12.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Link from "next/link";
|
||||
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { VideoTags } from "components/utils";
|
||||
import PlayerContext from "context/playerContext";
|
||||
import createVideoSlug from "utils/createVideoSlug";
|
||||
import { Link } from "gatsby";
|
||||
import { Icon } from "components/icon";
|
||||
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "components/button";
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<Link to={`/anime/${anime.slug}/${videoSlug}`}>
|
||||
<Button {...props}>
|
||||
<Link href={`/anime/${anime.slug}/${videoSlug}`} passHref>
|
||||
<Button as="a" {...props}>
|
||||
<Button as="span" variant="primary">
|
||||
<Icon icon={isPlaying ? faCompactDisc : faPlay} spin={isPlaying}/>
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { Button } from "./Button";
|
||||
export { VideoButton } from "./VideoButton"
|
||||
export { VideoButton } from "./VideoButton";
|
||||
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 { Button } from "components/button";
|
||||
import { Text } from "components/text";
|
||||
@@ -7,7 +7,7 @@ import createVideoSlug from "utils/createVideoSlug";
|
||||
import { Flex } from "components/box";
|
||||
import { Icon } from "components/icon";
|
||||
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 }) {
|
||||
const { smallCover } = useImage(anime);
|
||||
@@ -25,8 +25,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||
<SummaryCard.Description>
|
||||
<span>Anime</span>
|
||||
{!!anime.year && (
|
||||
<Link to={premiereLink}>
|
||||
<Text link>{premiere}</Text>
|
||||
<Link href={premiereLink} passHref>
|
||||
<Text as="a" link>{premiere}</Text>
|
||||
</Link>
|
||||
)}
|
||||
<span>{anime.themes.length} themes</span>
|
||||
@@ -41,10 +41,10 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||
to={animeLink}
|
||||
>
|
||||
{!hideThemes && (
|
||||
<Flex display={[ "none", "flex" ]} flexWrap="wrap" gapsBoth="0.75rem">
|
||||
<Flex display={["none", "flex"]} flexWrap="wrap" gapsBoth="0.75rem">
|
||||
{anime.themes
|
||||
.filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
|
||||
.sort(chain(themeTypeComparator, themeSequenceComparator))
|
||||
.sort(chain(themeTypeComparator, themeIndexComparator))
|
||||
.slice(0, maxThemes)
|
||||
.map((theme) => {
|
||||
const entry = theme.entries[0];
|
||||
@@ -52,8 +52,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||
const videoSlug = createVideoSlug(theme, entry, video);
|
||||
|
||||
return (
|
||||
<Link key={theme.slug} to={`/anime/${anime.slug}/${videoSlug}`}>
|
||||
<Button variant="on-card">
|
||||
<Link key={theme.slug} href={`/anime/${anime.slug}/${videoSlug}`} passHref>
|
||||
<Button as="a" variant="on-card">
|
||||
<Button as="span" variant="primary">
|
||||
<Icon icon={faPlay}/>
|
||||
</Button>
|
||||
@@ -64,8 +64,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||
})
|
||||
}
|
||||
{anime.themes.length > 4 && (
|
||||
<Link to={animeLink}>
|
||||
<Button variant="on-card" title="Show all themes">
|
||||
<Link href={animeLink} passHref>
|
||||
<Button as="a" variant="on-card" title="Show all themes">
|
||||
<Icon icon={faEllipsisH}/>
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -75,31 +75,3 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
|
||||
</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 styled, { css, keyframes } from "styled-components";
|
||||
import { Card } from "components/card";
|
||||
import { Flex } from "components/box";
|
||||
import withBasePath from "utils/withBasePath";
|
||||
|
||||
const loadingAnimation = keyframes`
|
||||
0% { background-position: 0 0; }
|
||||
100% { background-position: 100% 100%; }
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledCover = styled.img.attrs({
|
||||
loading: "lazy"
|
||||
})`
|
||||
width: 48px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
width: 48px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
|
||||
background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat;
|
||||
background-size: 500% 500%;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: ${loadingAnimation} 2s infinite alternate linear;
|
||||
}
|
||||
background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat;
|
||||
background-size: 500% 500%;
|
||||
|
||||
${(props) => props.placeholder && css`
|
||||
padding: 0.5rem;
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
`}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: ${loadingAnimation} 2s infinite alternate linear;
|
||||
}
|
||||
|
||||
${(props) => props.isPlaceholder && css`
|
||||
padding: 0.5rem;
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
`}
|
||||
`;
|
||||
|
||||
export function SummaryCard({ title, description, image, to, children, ...props }) {
|
||||
return (
|
||||
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}>
|
||||
<Link to={to}>
|
||||
<StyledCover alt="Cover" src={image || withPrefix("/img/logo.svg")} placeholder={!image} loading="lazy"/>
|
||||
<Link href={to}>
|
||||
<a>
|
||||
<StyledCover
|
||||
alt="Cover"
|
||||
src={image || withBasePath("/img/logo.svg")}
|
||||
isPlaceholder={!image}
|
||||
loading="lazy"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
<Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem">
|
||||
<Text fontWeight="600" maxLines={1}>
|
||||
{typeof title === "string" ? (
|
||||
<Link to={to}>
|
||||
<Text link>{title}</Text>
|
||||
<Link href={to} passHref>
|
||||
<Text as="a" link>{title}</Text>
|
||||
</Link>
|
||||
) : title}
|
||||
</Text>
|
||||
<Text variant="small" maxLines={1}>
|
||||
{typeof description === "string" ? (
|
||||
<SummaryCard.Description>
|
||||
{[ description ]}
|
||||
{[description]}
|
||||
</SummaryCard.Description>
|
||||
) : description}
|
||||
</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 { Text } from "components/text";
|
||||
import useImage from "hooks/useImage";
|
||||
import createVideoSlug from "utils/createVideoSlug";
|
||||
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)
|
||||
export function ThemeSummaryCard({ theme, artist }) {
|
||||
@@ -27,18 +42,24 @@ export function ThemeSummaryCard({ theme, artist }) {
|
||||
<SummaryCard.Description>
|
||||
<span>Theme</span>
|
||||
<span>{theme.slug}</span>
|
||||
<Link to={`/anime/${theme.anime.slug}`}>
|
||||
<Text link>{theme.anime.name}</Text>
|
||||
<Link href={`/anime/${theme.anime.slug}`} passHref>
|
||||
<Text as="a" link>{theme.anime.name}</Text>
|
||||
</Link>
|
||||
</SummaryCard.Description>
|
||||
);
|
||||
|
||||
return (
|
||||
<SummaryCard
|
||||
<StyledSummaryCard
|
||||
title={<SongTitleWithArtists song={theme.song} songTitleLinkTo={to} artist={artist}/>}
|
||||
description={description}
|
||||
image={smallCover}
|
||||
to={to}
|
||||
/>
|
||||
>
|
||||
<StyledShowOnHover>
|
||||
<Button variant="on-card" circle silent>
|
||||
<Icon icon={faEllipsisV}/>
|
||||
</Button>
|
||||
</StyledShowOnHover>
|
||||
</StyledSummaryCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { Card } from "./Card";
|
||||
export { SummaryCard } from "./SummaryCard";
|
||||
export { AnimeSummaryCard } from "./AnimeSummaryCard";
|
||||
export { ThemeSummaryCard } from "./ThemeSummaryCard";
|
||||
export { ArtistSummaryCard } from "./ArtistSummaryCard";
|
||||
export { SummaryCard } from "./SummaryCard";
|
||||
export { ThemeDetailCard } from "./ThemeDetailCard";
|
||||
export { ErrorCard } from "./ErrorCard";
|
||||
|
||||
@@ -36,4 +36,4 @@ DescriptionList.Item = function DescriptionListItem({ title, children }) {
|
||||
<StyledValue>{children}</StyledValue>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Flex } from "components/box";
|
||||
import { ThemeDetailCard } from "components/card";
|
||||
import { Listbox } from "components/listbox";
|
||||
import { HorizontalScroll } from "components/utils";
|
||||
import { chain, themeSequenceComparator, themeTypeComparator } from "utils/comparators";
|
||||
import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
|
||||
|
||||
export function AnimeThemeFilter({ themes }) {
|
||||
const groups = useMemo(() => themes.reduce((groups, theme) => {
|
||||
@@ -28,7 +28,7 @@ export function AnimeThemeFilter({ themes }) {
|
||||
|
||||
const filteredThemes = activeThemes
|
||||
.filter((theme) => !filterType || theme.type === filterType)
|
||||
.sort(chain(themeTypeComparator, themeSequenceComparator));
|
||||
.sort(chain(themeTypeComparator, themeIndexComparator));
|
||||
|
||||
return (
|
||||
<Box gapsColumn="1rem">
|
||||
|
||||
@@ -28,7 +28,7 @@ const StyledSearchInput = styled.div`
|
||||
`;
|
||||
const StyledForm = styled.form`
|
||||
flex: 1;
|
||||
`
|
||||
`;
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Link, withPrefix } from "gatsby";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
faBars,
|
||||
faLightbulb,
|
||||
@@ -22,14 +22,15 @@ import ColorThemeContext from "context/colorThemeContext";
|
||||
import { Icon } from "components/icon";
|
||||
import { Text } from "components/text";
|
||||
import { Flex } from "components/box";
|
||||
import { useLocation } from "@reach/router";
|
||||
import useCurrentSeason from "hooks/useCurrentSeason";
|
||||
import navigateToRandomTheme from "utils/navigateToRandomTheme";
|
||||
import { useRouter } from "next/router";
|
||||
import withBasePath from "utils/withBasePath";
|
||||
|
||||
export function Navigation({ offsetToggleButton = false }) {
|
||||
const [ show, setShow ] = useState(false);
|
||||
const { colorTheme, toggleColorTheme } = useContext(ColorThemeContext);
|
||||
const location = useLocation();
|
||||
const router = useRouter();
|
||||
|
||||
const { currentYear, currentSeason } = useCurrentSeason();
|
||||
|
||||
@@ -37,28 +38,30 @@ export function Navigation({ offsetToggleButton = false }) {
|
||||
// we want to close the modal navigation.
|
||||
useEffect(() => {
|
||||
setShow(false);
|
||||
}, [location]);
|
||||
}, [router.pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledNavigation show={show} onClick={() => setShow(false)}>
|
||||
<StyledNavigationContainer onClick={(event) => event.stopPropagation()}>
|
||||
<StyledLogoContainer to="/">
|
||||
<StyledLogo
|
||||
src={withPrefix("/img/logo.svg")}
|
||||
alt="Logo"
|
||||
width="277"
|
||||
height="150"
|
||||
/>
|
||||
</StyledLogoContainer>
|
||||
<Link href="/" passHref>
|
||||
<StyledLogoContainer>
|
||||
<StyledLogo
|
||||
src={withBasePath("/img/logo.svg")}
|
||||
alt="Logo"
|
||||
width="277"
|
||||
height="150"
|
||||
/>
|
||||
</StyledLogoContainer>
|
||||
</Link>
|
||||
<Flex
|
||||
flexDirection={[ "column", "row" ]}
|
||||
gapsRow={[ 0, "0.5rem" ]}
|
||||
gapsColumn={[ "0.5rem", 0 ]}
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Link to="/search">
|
||||
<Button variant="on-card" silent gapsRow="0.5rem">
|
||||
<Link href="/search" passHref>
|
||||
<Button as="a" variant="on-card" silent gapsRow="0.5rem">
|
||||
<Icon icon={faSearch}/>
|
||||
<Text>Search</Text>
|
||||
</Button>
|
||||
@@ -67,8 +70,8 @@ export function Navigation({ offsetToggleButton = false }) {
|
||||
<Icon icon={faRandom}/>
|
||||
<Text>Play Random</Text>
|
||||
</Button>
|
||||
<Link to={`/year/${currentYear}/${currentSeason}`}>
|
||||
<Button variant="on-card" silent gapsRow="0.5rem">
|
||||
<Link href={(currentYear && currentSeason) ? `/year/${currentYear}/${currentSeason}` : "/"} passHref>
|
||||
<Button as="a" variant="on-card" silent gapsRow="0.5rem">
|
||||
<Icon icon={faTv}/>
|
||||
<Text>Current Season</Text>
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import styled, { css } from "styled-components";
|
||||
import { Container } from "components/container";
|
||||
import { gapsColumn, gapsRow } from "styles/mixins";
|
||||
import { Link } from "gatsby";
|
||||
import { Button } from "components/button";
|
||||
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;
|
||||
align-items: center;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, navigate } from "gatsby";
|
||||
import { useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { Box, Flex } from "components/box";
|
||||
import { SEO } from "components/seo";
|
||||
import { Text } from "components/text";
|
||||
import { SearchInput } from "components/input";
|
||||
import { HorizontalScroll } from "components/utils";
|
||||
@@ -10,6 +9,7 @@ import { Icon } from "components/icon";
|
||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import styled from "styled-components";
|
||||
import theme from "theme";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const StyledSearchOptions = styled(Flex)`
|
||||
flex-wrap: wrap;
|
||||
@@ -20,36 +20,41 @@ const StyledSearchOptions = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
export function SearchNavigation({ entity, location: { search, hash, state } }) {
|
||||
const urlParams = useMemo(() => new URLSearchParams(search), [ search ]);
|
||||
const urlSuffix = search + hash;
|
||||
|
||||
const [ searchQuery, setSearchQuery ] = useState(urlParams.get("q") || "");
|
||||
export function SearchNavigation() {
|
||||
const router = useRouter();
|
||||
const { entity, ...urlParams } = router.query;
|
||||
const searchQuery = urlParams.q || "";
|
||||
|
||||
// Generates page title based on search query
|
||||
const pageTitle = searchQuery && searchQuery.trim()
|
||||
? `${searchQuery} - Search`
|
||||
: "Search";
|
||||
|
||||
useEffect(() => {
|
||||
const updateSearchQuery = (newSearchQuery) => {
|
||||
// Update URL to maintain the searchQuery on page navigation.
|
||||
const newUrlParams = new URLSearchParams();
|
||||
if (searchQuery) {
|
||||
newUrlParams.set("q", searchQuery);
|
||||
const newUrlParams = {
|
||||
...urlParams
|
||||
};
|
||||
|
||||
if (newSearchQuery) {
|
||||
newUrlParams.q = newSearchQuery;
|
||||
} else {
|
||||
delete newUrlParams.q;
|
||||
}
|
||||
const params = newUrlParams.toString();
|
||||
|
||||
let url = "/search";
|
||||
if (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
|
||||
}, [ entity, searchQuery ]);
|
||||
};
|
||||
|
||||
const inputRef = useRef();
|
||||
|
||||
@@ -64,13 +69,12 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
|
||||
|
||||
return (
|
||||
<Box gapsColumn="1.5rem">
|
||||
<SEO title={pageTitle} />
|
||||
<Text variant="h1">Search</Text>
|
||||
<StyledSearchOptions>
|
||||
<Box flex="1" minWidth="33%">
|
||||
<SearchInput
|
||||
query={searchQuery}
|
||||
setQuery={setSearchQuery}
|
||||
setQuery={updateSearchQuery}
|
||||
inputProps={{
|
||||
ref: inputRef,
|
||||
spellCheck: false
|
||||
@@ -84,15 +88,15 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
|
||||
>
|
||||
{({ Button, item, selected, content }) => item.value === null ? (
|
||||
!!entity ? (
|
||||
<Link key={null} to={`/search${urlSuffix}`}>
|
||||
<Button>
|
||||
<Link key={null} href={{ pathname: "/search", query: urlParams }} passHref>
|
||||
<Button as="a">
|
||||
<Icon icon={faTimes}/>
|
||||
</Button>
|
||||
</Link>
|
||||
) : null
|
||||
) : (
|
||||
<Link key={item.value} to={`/search/${item.value}${urlSuffix}`}>
|
||||
<Button variant={selected && "primary"}>
|
||||
<Link key={item.value} href={{ pathname: `/search/${item.value}`, query: urlParams }} passHref>
|
||||
<Button as="a" variant={selected && "primary"}>
|
||||
{content}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box } from "components/box";
|
||||
import { Switcher } from "components/switcher";
|
||||
import { Link } from "gatsby";
|
||||
import Link from "next/link";
|
||||
|
||||
export function SeasonNavigation({ year, season, seasonList }) {
|
||||
return (
|
||||
@@ -11,8 +11,8 @@ export function SeasonNavigation({ year, season, seasonList }) {
|
||||
mx="auto"
|
||||
>
|
||||
{({ Button, item, selected, content }) => (
|
||||
<Link key={item.value} to={`/year/${year}/${item.value}`}>
|
||||
<Button variant={selected && "primary"}>
|
||||
<Link key={item.value} href={`/year/${year}/${item.value}`} passHref>
|
||||
<Button as="a" variant={selected && "primary"}>
|
||||
{content}
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { graphql, Link, useStaticQuery } from "gatsby";
|
||||
import Link from "next/link";
|
||||
import { Button } from "components/button";
|
||||
import styled from "styled-components";
|
||||
import { Flex } from "components/box";
|
||||
@@ -18,17 +18,7 @@ const StyledYearNext = styled(StyledYear)`
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
export function YearNavigation({ year }) {
|
||||
const yearList = useStaticQuery(graphql`
|
||||
query {
|
||||
allAnime {
|
||||
groupedByYear: group(field: year) {
|
||||
year: fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
`).allAnime.groupedByYear.map((group) => +group.year);
|
||||
|
||||
export function YearNavigation({ year, yearList }) {
|
||||
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;
|
||||
|
||||
@@ -36,20 +26,20 @@ export function YearNavigation({ year }) {
|
||||
<Flex alignItems="center">
|
||||
<StyledYearPrevious>
|
||||
{previousYear && (
|
||||
<Link to={`/year/${previousYear}`}>
|
||||
<Button silent>{previousYear}</Button>
|
||||
<Link href={`/year/${previousYear}`} passHref>
|
||||
<Button as="a" silent>{previousYear}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</StyledYearPrevious>
|
||||
<Link to={`/year`}>
|
||||
<Button silent>
|
||||
<Link href={`/year`} passHref>
|
||||
<Button as="a" silent>
|
||||
<Text variant="h1">{year}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
<StyledYearNext>
|
||||
{nextYear && (
|
||||
<Link to={`/year/${nextYear}`}>
|
||||
<Button silent>{nextYear}</Button>
|
||||
<Link href={`/year/${nextYear}`} passHref>
|
||||
<Button as="a" silent>{nextYear}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</StyledYearNext>
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { Text } from "components/text";
|
||||
import { Listbox } from "components/listbox";
|
||||
import { Flex } from "components/box";
|
||||
import { graphql, useStaticQuery } from "gatsby";
|
||||
|
||||
export function SearchFilterYear({ value, setValue }) {
|
||||
const yearList = useStaticQuery(graphql`
|
||||
query {
|
||||
allAnime {
|
||||
groupedByYear: group(field: year) {
|
||||
year: fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
`).allAnime.groupedByYear
|
||||
.map((node) => node.year)
|
||||
.sort()
|
||||
.reverse();
|
||||
const yearList = [ 2021, 2020, 2019 ];
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="stretch" gapsColumn="0.5rem">
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
} from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { navigate } from "gatsby";
|
||||
import { AnimeSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
|
||||
const sortByFields = new Map([
|
||||
[ "A ➜ Z", "name" ],
|
||||
@@ -18,29 +18,23 @@ const sortByFields = new Map([
|
||||
]);
|
||||
const sortByOptions = [ ...sortByFields.keys() ];
|
||||
|
||||
export function SearchAnime({ searchQuery, locationState }) {
|
||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
||||
const filterSeason = locationState?.filterSeason || null;
|
||||
const filterYear = locationState?.filterYear || null;
|
||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
season: null,
|
||||
year: null,
|
||||
sortBy: sortByOptions[0]
|
||||
};
|
||||
|
||||
const updateState = (field) => (newValue) => {
|
||||
navigate("", {
|
||||
state: {
|
||||
...locationState,
|
||||
[field]: newValue
|
||||
},
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
export function SearchAnime({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||
|
||||
const entitySearch = useEntitySearch("anime", searchQuery, {
|
||||
filters: {
|
||||
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
||||
season: filterSeason,
|
||||
year: filterYear
|
||||
"name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
season: filter.season,
|
||||
year: filter.year
|
||||
},
|
||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
||||
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -48,13 +42,13 @@ export function SearchAnime({ searchQuery, locationState }) {
|
||||
searchQuery={searchQuery}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
||||
<SearchFilterSeason value={filterSeason} setValue={updateState("filterSeason")}/>
|
||||
<SearchFilterYear value={filterYear} setValue={updateState("filterYear")}/>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSeason value={filter.season} setValue={updateFilter("season")}/>
|
||||
<SearchFilterYear value={filter.year} setValue={updateFilter("year")}/>
|
||||
<SearchFilterSortBy
|
||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||
value={searchQuery ? "Relevance" : sortBy}
|
||||
setValue={updateState("sortBy")}
|
||||
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||
setValue={updateFilter("sortBy")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { navigate } from "gatsby";
|
||||
import { ArtistSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
|
||||
const sortByFields = new Map([
|
||||
[ "A ➜ Z", "name" ],
|
||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
||||
]);
|
||||
const sortByOptions = [ ...sortByFields.keys() ];
|
||||
|
||||
export function SearchArtist({ searchQuery, locationState }) {
|
||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: sortByOptions[0]
|
||||
};
|
||||
|
||||
const updateState = (field) => (newValue) => {
|
||||
navigate("", {
|
||||
state: {
|
||||
...locationState,
|
||||
[field]: newValue
|
||||
},
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
export function SearchArtist({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||
|
||||
const entitySearch = useEntitySearch("artist", searchQuery, {
|
||||
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 (
|
||||
@@ -37,11 +31,11 @@ export function SearchArtist({ searchQuery, locationState }) {
|
||||
searchQuery={searchQuery}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy
|
||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||
value={searchQuery ? "Relevance" : sortBy}
|
||||
setValue={updateState("sortBy")}
|
||||
value={searchQuery ? "Relevance" : filter.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 { Button } from "components/button";
|
||||
import { Text } from "components/text";
|
||||
import { Box, Flex } from "components/box";
|
||||
import { Link } from "gatsby";
|
||||
import { Icon } from "components/icon";
|
||||
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 { useRouter } from "next/router";
|
||||
|
||||
export function SearchGlobal({ searchQuery }) {
|
||||
const fetchSearchResults = () => fetchGlobalSearchResults(
|
||||
searchQuery,
|
||||
4,
|
||||
[ "anime", "theme", "artist", "series", "studio" ]
|
||||
["anime", "theme", "artist", "series", "studio"]
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -22,7 +22,7 @@ export function SearchGlobal({ searchQuery }) {
|
||||
isLoading,
|
||||
isError
|
||||
} = useQuery(
|
||||
[ "searchGlobal", searchQuery ],
|
||||
["searchGlobal", searchQuery],
|
||||
fetchSearchResults,
|
||||
{
|
||||
keepPreviousData: true
|
||||
@@ -87,7 +87,8 @@ export function SearchGlobal({ searchQuery }) {
|
||||
title="Series"
|
||||
results={seriesResults}
|
||||
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
|
||||
@@ -95,7 +96,8 @@ export function SearchGlobal({ searchQuery }) {
|
||||
title="Studios"
|
||||
results={studioResults}
|
||||
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>
|
||||
@@ -103,8 +105,8 @@ export function SearchGlobal({ searchQuery }) {
|
||||
}
|
||||
|
||||
function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
|
||||
const { search, hash } = useLocation();
|
||||
const urlSuffix = search + hash;
|
||||
const router = useRouter();
|
||||
const urlParams = router.query;
|
||||
|
||||
if (!results.length) {
|
||||
return null;
|
||||
@@ -121,8 +123,8 @@ function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
|
||||
</Box>
|
||||
{hasMoreResults && (
|
||||
<Flex justifyContent="center">
|
||||
<Link to={`/search/${entity}${urlSuffix}`}>
|
||||
<Button silent title="See all results">
|
||||
<Link href={{ pathname: `/search/${entity}`, query: urlParams }} passHref>
|
||||
<Button as="a" silent title="See all results">
|
||||
<Icon icon={faChevronDown}/>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { navigate } from "gatsby";
|
||||
import { SummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
|
||||
const sortByFields = new Map([
|
||||
[ "A ➜ Z", "name" ],
|
||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
||||
]);
|
||||
const sortByOptions = [ ...sortByFields.keys() ];
|
||||
|
||||
export function SearchSeries({ searchQuery, locationState }) {
|
||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: sortByOptions[0]
|
||||
};
|
||||
|
||||
const updateState = (field) => (newValue) => {
|
||||
navigate("", {
|
||||
state: {
|
||||
...locationState,
|
||||
[field]: newValue
|
||||
},
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
export function SearchSeries({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||
|
||||
const entitySearch = useEntitySearch("series", searchQuery, {
|
||||
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 (
|
||||
@@ -37,11 +31,11 @@ export function SearchSeries({ searchQuery, locationState }) {
|
||||
searchQuery={searchQuery}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy
|
||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||
value={searchQuery ? "Relevance" : sortBy}
|
||||
setValue={updateState("sortBy")}
|
||||
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||
setValue={updateFilter("sortBy")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { navigate } from "gatsby";
|
||||
import { SummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
|
||||
const sortByFields = new Map([
|
||||
[ "A ➜ Z", "name" ],
|
||||
@@ -11,25 +11,19 @@ const sortByFields = new Map([
|
||||
]);
|
||||
const sortByOptions = [ ...sortByFields.keys() ];
|
||||
|
||||
export function SearchStudio({ searchQuery, locationState }) {
|
||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
sortBy: sortByOptions[0]
|
||||
};
|
||||
|
||||
const updateState = (field) => (newValue) => {
|
||||
navigate("", {
|
||||
state: {
|
||||
...locationState,
|
||||
[field]: newValue
|
||||
},
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
export function SearchStudio({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||
|
||||
const entitySearch = useEntitySearch("studio", searchQuery, {
|
||||
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 (
|
||||
@@ -37,11 +31,11 @@ export function SearchStudio({ searchQuery, locationState }) {
|
||||
searchQuery={searchQuery}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterSortBy
|
||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||
value={searchQuery ? "Relevance" : sortBy}
|
||||
setValue={updateState("sortBy")}
|
||||
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||
setValue={updateFilter("sortBy")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter";
|
||||
import useEntitySearch from "hooks/useEntitySearch";
|
||||
import { SearchEntity } from "components/search";
|
||||
import { navigate } from "gatsby";
|
||||
import { ThemeSummaryCard } from "components/card";
|
||||
import useSessionStorage from "hooks/useSessionStorage";
|
||||
|
||||
const sortByFields = new Map([
|
||||
[ "A ➜ Z", "song.title" ],
|
||||
@@ -13,28 +13,22 @@ const sortByFields = new Map([
|
||||
]);
|
||||
const sortByOptions = [ ...sortByFields.keys() ];
|
||||
|
||||
export function SearchTheme({ searchQuery, locationState }) {
|
||||
const filterFirstLetter = locationState?.filterFirstLetter || null;
|
||||
const filterType = locationState?.filterType || null;
|
||||
const sortBy = locationState?.sortBy || sortByOptions[0];
|
||||
const initialFilter = {
|
||||
firstLetter: null,
|
||||
type: null,
|
||||
sortBy: sortByOptions[0]
|
||||
};
|
||||
|
||||
const updateState = (field) => (newValue) => {
|
||||
navigate("", {
|
||||
state: {
|
||||
...locationState,
|
||||
[field]: newValue
|
||||
},
|
||||
replace: true
|
||||
});
|
||||
};
|
||||
export function SearchTheme({ searchQuery }) {
|
||||
const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
|
||||
|
||||
const entitySearch = useEntitySearch("theme", searchQuery, {
|
||||
filters: {
|
||||
has: "song",
|
||||
"song][title][like": filterFirstLetter ? `${filterFirstLetter}%` : null,
|
||||
type: filterType
|
||||
"song][title][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
|
||||
type: filter.type
|
||||
},
|
||||
sortBy: searchQuery ? null : sortByFields.get(sortBy)
|
||||
sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -42,12 +36,12 @@ export function SearchTheme({ searchQuery, locationState }) {
|
||||
searchQuery={searchQuery}
|
||||
filters={
|
||||
<>
|
||||
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/>
|
||||
<SearchFilterThemeType value={filterType} setValue={updateState("filterType")}/>
|
||||
<SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
|
||||
<SearchFilterThemeType value={filter.type} setValue={updateFilter("type")}/>
|
||||
<SearchFilterSortBy
|
||||
options={searchQuery ? [ "Relevance" ] : sortByOptions}
|
||||
value={searchQuery ? "Relevance" : sortBy}
|
||||
setValue={updateState("sortBy")}
|
||||
value={searchQuery ? "Relevance" : filter.sortBy}
|
||||
setValue={updateFilter("sortBy")}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,59 +1,23 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useLocation } from "@reach/router";
|
||||
import { Helmet } from "react-helmet";
|
||||
import useSiteMeta from "hooks/useSiteMeta";
|
||||
import Head from "next/head";
|
||||
import withBasePath from "utils/withBasePath";
|
||||
|
||||
export function SEO({ title, description, meta = [], lang }) {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const {
|
||||
description: defaultDescription,
|
||||
lang: defaultLanguage,
|
||||
titleTemplate,
|
||||
siteName,
|
||||
siteUrl
|
||||
} = useSiteMeta();
|
||||
|
||||
const seo = {
|
||||
url: `${siteUrl}${pathname}`,
|
||||
lang: lang || defaultLanguage,
|
||||
title: title || siteName,
|
||||
description: description || defaultDescription
|
||||
}
|
||||
export function SEO({
|
||||
title,
|
||||
description = "AnimeThemes is a simple and consistent repository of anime opening and ending themes.",
|
||||
image = withBasePath("/img/logo.svg"),
|
||||
children
|
||||
}) {
|
||||
const titleWithSuffix = title ? `${title} · AnimeThemes` : "AnimeThemes";
|
||||
|
||||
return (
|
||||
<Helmet
|
||||
htmlAttributes={{
|
||||
lang: seo.lang
|
||||
}}
|
||||
|
||||
title={seo.title}
|
||||
titleTemplate={title ? titleTemplate : null}
|
||||
meta={[
|
||||
{
|
||||
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
|
||||
<Head>
|
||||
<title>{titleWithSuffix}</title>
|
||||
<meta property="description" content={description}/>
|
||||
<meta property="og:title" content={titleWithSuffix}/>
|
||||
<meta property="og:description" content={description}/>
|
||||
<meta property="og:image" content={image}/>
|
||||
<meta property="og:site_name" content="AnimeThemes"/>
|
||||
{children}
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import styled from "styled-components";
|
||||
import { AnimateSharedLayout, motion } from "framer-motion";
|
||||
import { AnimateSharedLayout, m } from "framer-motion";
|
||||
import { Box } from "components/box";
|
||||
import { Button } from "components/button";
|
||||
import theme from "theme";
|
||||
@@ -26,7 +26,7 @@ const StyledButton = styled(Button)`
|
||||
background-color: transparent;
|
||||
}
|
||||
`;
|
||||
const StyledButtonBackground = styled(motion.div)`
|
||||
const StyledButtonBackground = styled(m.div)`
|
||||
position: absolute;
|
||||
z-index: ${theme.zIndices.switcherButton};
|
||||
top: 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { m } from "framer-motion";
|
||||
|
||||
export function Collapse({ collapse, children }) {
|
||||
if (collapse) {
|
||||
@@ -6,12 +6,12 @@ export function Collapse({ collapse, children }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<m.div
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ type: "tween", duration: 0.35, ease: [ 0, 0.66, 0.46, 0.98 ] }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</m.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Fragment } from "react";
|
||||
import { Link } from "gatsby";
|
||||
import Link from "next/link";
|
||||
import { Text } from "components/text";
|
||||
|
||||
// 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>
|
||||
{songTitleLinkTo
|
||||
? (
|
||||
<Link to={songTitleLinkTo}>
|
||||
<Text link italics={!song.title}>{songTitle}</Text>
|
||||
<Link href={songTitleLinkTo} passHref>
|
||||
<Text as="a" link italics={!song.title}>{songTitle}</Text>
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
@@ -27,8 +27,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
||||
{!!performedAs.as && (
|
||||
<>
|
||||
<Text variant="small" color="text-muted"> as </Text>
|
||||
<Link to={`/artist/${performedAs.artist.slug}`}>
|
||||
<Text link>
|
||||
<Link href={`/artist/${performedAs.artist.slug}`} passHref>
|
||||
<Text as="a" link>
|
||||
{performedAs.as}
|
||||
</Text>
|
||||
</Link>
|
||||
@@ -39,8 +39,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
||||
<Text variant="small" color="text-muted"> with </Text>
|
||||
{performedWith.map((performance, index) => (
|
||||
<Fragment key={performance.artist.slug}>
|
||||
<Link to={`/artist/${performance.artist.slug}`}>
|
||||
<Text link>
|
||||
<Link href={`/artist/${performance.artist.slug}`} passHref>
|
||||
<Text as="a" link>
|
||||
{performance.as || performance.artist.name}
|
||||
</Text>
|
||||
</Link>
|
||||
@@ -60,8 +60,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
|
||||
<Text variant="small" color="text-muted"> by </Text>
|
||||
{song.performances.map((performance, index) => (
|
||||
<Fragment key={performance.artist.slug}>
|
||||
<Link to={`/artist/${performance.artist.slug}`}>
|
||||
<Text link>
|
||||
<Link href={`/artist/${performance.artist.slug}`} passHref>
|
||||
<Text as="a" link>
|
||||
{performance.as || performance.artist.name}
|
||||
</Text>
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export { AspectRatio } from "./AspectRatio";
|
||||
export { Collapse } from "./Collapse";
|
||||
export { HeightTransition } from "./HeightTransition";
|
||||
export { SongTitleWithArtists } from "./SongTitleWithArtists"
|
||||
export { SongTitleWithArtists } from "./SongTitleWithArtists";
|
||||
export { ThemeEntryTags } from "./ThemeEntryTags";
|
||||
export { VideoTags } from "./VideoTags";
|
||||
export { HorizontalScroll } from "./HorizontalScroll";
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
StyledOverlay, StyledPlayerInfo, StyledPlayerProgress, StyledPlayerProgressBar
|
||||
} from "./VideoPlayer.style";
|
||||
import { faDownload, faExpandAlt, faPause, faPlay, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { navigate } from "gatsby";
|
||||
import PlayerContext from "context/playerContext";
|
||||
import createVideoSlug from "utils/createVideoSlug";
|
||||
import { useMedia } from "use-media";
|
||||
@@ -17,8 +16,9 @@ import { Text } from "components/text";
|
||||
import { Flex } from "components/box";
|
||||
import { Container } from "components/container";
|
||||
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 }) {
|
||||
const [isPlaying, setPlaying] = useState(false);
|
||||
@@ -28,6 +28,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
|
||||
const { setCurrentVideo } = useContext(PlayerContext);
|
||||
const isMobile = useMedia({ maxWidth: "720px" });
|
||||
const videoUrl = `${videoBaseUrl}/video/${video.basename}`;
|
||||
const router = useRouter();
|
||||
|
||||
function togglePlay() {
|
||||
if (isPlaying) {
|
||||
@@ -39,7 +40,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
|
||||
|
||||
function maximize() {
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
import { motion } from "framer-motion";
|
||||
import { m } from "framer-motion";
|
||||
import { gapsColumn, gapsRow } from "styles/mixins";
|
||||
import { Button } from "components/button";
|
||||
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`
|
||||
position: fixed;
|
||||
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 = [
|
||||
"winter",
|
||||
"spring",
|
||||
"summer",
|
||||
"fall"
|
||||
];
|
||||
let cache = null;
|
||||
|
||||
export default function useCurrentSeason() {
|
||||
const { currentSeasonAnime } = useStaticQuery(graphql`
|
||||
query {
|
||||
currentSeasonAnime: allAnime {
|
||||
groupedByYear: group(field: year) {
|
||||
year: fieldValue
|
||||
nodes {
|
||||
season
|
||||
}
|
||||
}
|
||||
}
|
||||
const [ current, setCurrent ] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cache) {
|
||||
fetchCurrentSeason()
|
||||
.then((current) => {
|
||||
cache = current;
|
||||
|
||||
setCurrent(current);
|
||||
});
|
||||
} else {
|
||||
setCurrent(cache);
|
||||
}
|
||||
`);
|
||||
}, []);
|
||||
|
||||
const currentYearGroup = currentSeasonAnime.groupedByYear.sort((a, b) => b.year - a.year)[0];
|
||||
const currentYear = currentYearGroup.year;
|
||||
const currentSeason = seasonOrder[currentYearGroup.nodes.map(({ season }) => seasonOrder.indexOf(season.toLowerCase())).sort().pop()];
|
||||
|
||||
return { currentYear, currentSeason };
|
||||
return {
|
||||
currentYear: current?.year,
|
||||
currentSeason: current?.season.toLowerCase()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchEntitySearchResults } from "lib/search";
|
||||
import { fetchEntitySearchResults } from "lib/client/search";
|
||||
import { useInfiniteQuery, useQueryClient } from "react-query";
|
||||
|
||||
export default function useEntitySearch(entity, query, params = {}) {
|
||||
|
||||
@@ -5,10 +5,10 @@ export default function useImage(resourceWithImages) {
|
||||
for (const image of resourceWithImages.images) {
|
||||
switch (image.facet) {
|
||||
case "Small Cover":
|
||||
images.smallCover = image.link;
|
||||
images.smallCover = image.link.replace("localhost", "staging.animethemes.moe");
|
||||
break;
|
||||
case "Large Cover":
|
||||
images.largeCover = image.link;
|
||||
images.largeCover = image.link.replace("localhost", "staging.animethemes.moe");
|
||||
break;
|
||||
default:
|
||||
// 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 = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { baseUrl } from "gatsby-source-animethemes/src";
|
||||
import { baseUrl } from "lib/client/api";
|
||||
|
||||
const entityConfigs = {
|
||||
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()
|
||||
}
|
||||
};
|
||||