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.
This commit is contained in:
Manuel S
2021-11-24 00:13:50 +01:00
parent 45f5b157d2
commit 31aee782b0
129 changed files with 12995 additions and 48761 deletions
-20
View File
@@ -1,20 +0,0 @@
{
"presets": [
[
"babel-preset-gatsby",
{
"reactRuntime": "automatic"
}
]
],
"plugins": [
[
"prismjs",
{
"languages": [
"jsx"
]
}
]
]
}
+1
View File
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=http://localhost
+1
View File
@@ -0,0 +1 @@
NEXT_PUBLIC_API_URL=https://staging.animethemes.moe
-10
View File
@@ -1,10 +0,0 @@
# NPM
/node_modules
# Gatsby
/.cache
/public
# IDE (WebStorm)
*.iml
/.idea
+44 -43
View File
@@ -1,46 +1,47 @@
{ {
"globals": { "extends": [
"__PATH_PREFIX__": true "next/core-web-vitals",
}, "plugin:react/recommended"
"extends": [ ],
"react-app", "rules": {
"plugin:react/recommended" "semi": [
"error",
"always"
], ],
"rules": { "quotes": [
"quotes": [ "error",
"error", "double",
"double", {
{ "allowTemplateLiterals": true
"allowTemplateLiterals": true }
} ],
], "curly": [
"curly": [ "error",
"error", "all"
"all" ],
], "object-curly-spacing": [
"object-curly-spacing": [ "error",
"error", "always"
"always" ],
], "indent": [
"indent": [ "error",
"error", 4,
4, {
{ "SwitchCase": 1
"SwitchCase": 1 }
} ],
], "no-duplicate-imports": "error",
"no-duplicate-imports": "error", "no-restricted-imports": [
"no-restricted-imports": [ "error",
"error", {
{ "patterns": [
"patterns": [ "components/*/*"
"components/*/*" ]
] }
} ],
], "react/react-in-jsx-scope": "off",
"react/react-in-jsx-scope": "off", "react/self-closing-comp": "error",
"react/self-closing-comp": "error", // For now we don't habe prop-types validation
// For now we don't habe prop-types validation "react/prop-types": "off"
"react/prop-types": "off" }
}
} }
-60
View File
@@ -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
+32 -9
View File
@@ -1,14 +1,37 @@
# General # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env
.env.*
# NPM # dependencies
node_modules /node_modules
/.pnp
.pnp.js
# Gatsby # testing
/.cache /coverage
/public
/schema.json # next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# IDE (WebStorm) # IDE (WebStorm)
*.iml *.iml
-3
View File
@@ -1,3 +0,0 @@
{
"schemaPath": "schema.json"
}
-21
View File
@@ -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.
+44 -27
View File
@@ -1,41 +1,58 @@
# AnimeThemes Web Client # AnimeThemes Web Client
1. [Configuration](#configuration)
2. [Development](#development)
3. [Deployment](#deployment)
4. [Used technologies](#used-technologies)
## Configuration
To get started you need to define some environment variables. This can be done by creating a `.env.local` file in the
root directory.
This is a list of all available options:
```ini
; Datebase configuration
; Only username and password are required.
; All other fields take the specified values by default, but can be changed if wanted.
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=...
DB_PASSWORD=...
DB_DATABASE=animethemes
; (Optional) The URL to the AnimeThemes API to use.
; For development this is "http://localhost" by default.
; For production this is "https://staging.animethemes.moe" by default.
NEXT_PUBLIC_API_URL=http://localhost
; (Optional) The URL from which video files should be served.
; By default this is "https://animethemes.moe".
NEXT_PUBLIC_VIDEO_URL=https://animethemes.moe
; (Optional) The base path the app should be hosted on.
; By default the app is hosted on the root path (/).
NEXT_PUBLIC_BASE_PATH=/wiki
```
For more information on environment variables see the [Next.js documentation](https://nextjs.org/docs/basic-features/environment-variables).
## Development ## Development
1. `npm install` 1. `npm install`
2. `npm run develop` 2. `npm run dev`
## Deployment ## Deployment
### Hosting on root path 1. `npm install` (If not done already.)
2. `npm run build` generates all static pages and produces a production ready bundle.
1. `npm run build` 3. `npm run start` starts the server.
2. Upload the contents of `public` to your server. 4. The server is now listening on port 3000 for incoming requests.
### Hosting on sub-path
1. Adjust `pathPrefix` in `gatsby-config.js` to your server environment.
2. `npm run build-with-paths`
3. Upload the contents of `public` to your server.
## Configuration
You can configure certain settings by creating a `.env.development` (for development) or `.env.production` (for deployment)
in the client root directory and include the following:
```ini
; The URL to the AnimeThemes API to use.
; If not specified, "https://staging.animethemes.moe" is used as a default.
GATSBY_API_URL = http://localhost
```
### Apache Configuration
`.htaccess` files can be placed in `static` and will be copied to the `public` folder during build.
## Used technologies ## Used technologies
- [Gatsby](https://www.gatsbyjs.com/) - [Next.js](https://www.nextjs.org/)
- [styled-components](https://styled-components.com/) - [styled-components](https://styled-components.com/)
- [react-query](https://react-query.tanstack.com/) - [react-query](https://react-query.tanstack.com/)
- [Font Awesome](https://fontawesome.com/) - [Font Awesome](https://fontawesome.com/)
-6
View File
@@ -1,6 +0,0 @@
export function shouldUpdateScroll({
routerProps,
prevRouterProps
}) {
return routerProps?.location?.pathname !== prevRouterProps?.location?.pathname;
}
-66
View File
@@ -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;
-3
View File
@@ -1,3 +0,0 @@
// eslint-disable-next-line no-native-reassign
require = require("esm")(module);
module.exports = require("./gatsby-config.esm.js");
-142
View File
@@ -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 }
});
}
}
-3
View File
@@ -1,3 +0,0 @@
// eslint-disable-next-line no-native-reassign
require = require("esm")(module);
module.exports = require("./gatsby-node.esm.js");
-24
View File
@@ -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 />);
}
+5
View File
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "src"
}
}
+11
View File
@@ -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
}
});
+10438 -32794
View File
File diff suppressed because it is too large Load Diff
+39 -55
View File
@@ -1,57 +1,41 @@
{ {
"name": "anime-themes", "name": "animethemes-web",
"version": "1.0.0", "version": "2.0.0",
"scripts": { "private": true,
"develop": "gatsby develop", "scripts": {
"build": "gatsby build", "dev": "next dev",
"build-with-paths": "gatsby build --prefix-paths" "build": "next build",
}, "start": "next start",
"dependencies": { "lint": "next lint"
"@fontsource/nunito": "^4.5.0", },
"@fortawesome/fontawesome-svg-core": "^1.2.34", "dependencies": {
"@fortawesome/free-brands-svg-icons": "^5.15.2", "@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-solid-svg-icons": "^5.15.2", "@fortawesome/free-brands-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14", "@fortawesome/free-solid-svg-icons": "^5.15.2",
"@reach/listbox": "^0.16.1", "@fortawesome/react-fontawesome": "^0.1.14",
"@react-hook/resize-observer": "^1.2.2", "@graphql-tools/merge": "^8.2.1",
"babel-plugin-prismjs": "^2.0.1", "@graphql-tools/schema": "^8.2.0",
"babel-plugin-styled-components": "^1.11.1", "@next/bundle-analyzer": "^12.0.3",
"babel-preset-gatsby": "^1.3.0", "@reach/listbox": "^0.16.1",
"common-tags": "^1.8.0", "common-tags": "^1.8.0",
"dotenv": "^8.2.0", "framer-motion": "^4.1.17",
"esm": "^3.2.25", "graphql": "^15.6.1",
"framer-motion": "^4.1.3", "knex": "^0.95.11",
"gatsby": "^3.13.0", "lodash": "^4.17.21",
"gatsby-plugin-fontawesome-css": "^1.1.0", "mysql2": "^2.3.3",
"gatsby-plugin-layout": "^2.3.0", "next": "^12.0.5-canary.6",
"gatsby-plugin-manifest": "^3.3.0", "prismjs": "^1.25.0",
"gatsby-plugin-react-helmet": "^4.3.0", "react": "^17.0.2",
"gatsby-plugin-root-import": "^2.0.6", "react-dom": "^17.0.2",
"gatsby-plugin-sass": "^4.3.0", "react-query": "^3.29.0",
"gatsby-plugin-styled-components": "^4.3.0", "sass": "^1.43.4",
"gatsby-source-animethemes": "file:plugins/gatsby-source-animethemes", "styled-components": "^5.3.3",
"gatsby-source-animethemes-db": "file:plugins/gatsby-source-animethemes-db", "styled-system": "^5.1.5",
"luxon": "^2.0.2", "use-debounce": "^7.0.0",
"node-fetch": "^2.6.1", "use-media": "^1.4.0"
"prismjs": "^1.24.1", },
"react": "^17.0.1", "devDependencies": {
"react-dom": "^17.0.1", "eslint": "7.32.0",
"react-helmet": "^6.1.0", "eslint-config-next": "^12.0.3"
"react-query": "^3.18.1", }
"sass": "^1.32.8",
"styled-components": "^5.2.0",
"styled-system": "^5.1.5",
"use-debounce": "^6.0.1",
"use-media": "^1.4.0"
},
"devDependencies": {
"@hot-loader/react-dom": "^17.0.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.21.0",
"eslint-config-react-app": "^6.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-react": "^7.22.0",
"gatsby-plugin-eslint": "^3.0.0",
"gatsby-plugin-extract-schema": "^0.1.0"
}
} }
@@ -1,157 +0,0 @@
const fetch = require("node-fetch");
const fs = require("fs");
const mappings = JSON.parse(fs.readFileSync("./plugins/gatsby-source-animebracket/mappings.json", "utf8"));
exports.onPreInit = ({ reporter }) => reporter.info("Loaded gatsby-source-animebracket");
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
createTypes(`
type Bracket implements Node {
slug: String!
name: String!
currentRound: BracketRound @link(by: "id")
currentGroup: Int
rounds: [BracketRound] @link(by: "bracket.id", from: "id")
}
type BracketRound implements Node {
bracket: Bracket @link(by: "id")
tier: Int!
name: String
pairings: [BracketPairing] @link(by: "round.id", from: "id")
}
type BracketPairing implements Node {
round: BracketRound @link(by: "id")
order: Int!
group: Int!
characterA: BracketCharacter @link(by: "id")
characterB: BracketCharacter @link(by: "id")
votesA: Int
votesB: Int
}
type BracketCharacter implements Node {
pairing: BracketPairing @link(by: "id")
idRaw: Int!
seed: Int!
name: String!
source: String!
theme: Theme @link(by: "idRaw")
}
`);
};
exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const helpers = {
...actions,
createNodeId,
createContentDigest
};
const perma = "best-anime-opening-ix-salty-arrow";
const bracketResponse = await fetch(`https://animebracket.com/api/bracket/${perma}`);
const bracket = await bracketResponse.json();
const roundsResponse = await fetch(`https://animebracket.com/api/results/${perma}`)
const rounds = await roundsResponse.json();
const currentRoundResponse = await fetch(`https://animebracket.com/api/rounds/${perma}`);
const currentRound = await currentRoundResponse.json();
const bracketNode = createNodeFromData({
id: bracket.id,
slug: perma,
name: bracket.name,
currentRound: currentRound ? createNodeId(`BracketRound-${bracket.id}-${currentRound[0].tier}`) : null,
currentGroup: currentRound ? currentRound[0].group : null,
rounds: rounds.map((round) => createNodeId(`BracketRound-${bracket.id}-${round[0].tier}`))
}, "Bracket", helpers);
for (const round of rounds) {
sourceRound(round);
}
if (currentRound) {
sourceRound(currentRound);
}
function sourceRound(round) {
const tier = +round[0].tier;
const roundNode = createNodeFromData({
id: `${bracket.id}-${tier}`,
bracket: bracketNode.id,
tier: tier,
pairings: round.map((pairing) => createNodeId(`BracketPairing-${pairing.id}`))
}, "BracketRound", helpers);
for (const pairing of round) {
if (pairing.filler) {
continue;
}
const pairingNode = createNodeFromData({
id: pairing.id,
round: roundNode.id,
order: +pairing.order,
group: +pairing.group,
characterA: createNodeId(`BracketCharacter-${pairing.character1.id}`),
characterB: createNodeId(`BracketCharacter-${pairing.character2.id}`),
votesA: pairing.character1.votes || null,
votesB: pairing.character2.votes || null
}, "BracketPairing", helpers);
for (const character of [ pairing.character1, pairing.character2 ]) {
createNodeFromData({
id: character.id,
idRaw: character.id,
pairing: pairingNode.id,
seed: character.seed,
name: character.name,
source: character.source,
theme: mappings[character.id]
}, "BracketCharacter", helpers);
}
}
}
};
exports.createResolvers = ({ createResolvers }) => {
createResolvers({
BracketRound: {
name: {
resolve(source) {
switch (source.pairings.length) {
case 1:
return "Finals";
case 2:
return "Semifinals";
case 4:
return "Quarterfinals";
default:
return null;
}
}
}
}
});
};
function createNodeFromData(item, nodeType, helpers) {
const node = {
...item,
id: helpers.createNodeId(`${nodeType}-${item.id}`),
parent: null,
children: [],
internal: {
type: nodeType,
content: JSON.stringify(item),
contentDigest: helpers.createContentDigest(item)
}
};
helpers.createNode(node)
return node;
}
@@ -1,4 +0,0 @@
{
"name": "gatsby-source-animebracket",
"version": "1.0.0"
}
@@ -1,7 +0,0 @@
exports.onPreInit = ({ reporter }) => reporter.info("Loaded gatsby-source-animethemes-db");
exports.createSchemaCustomization = require("./src/create-schema-customization");
exports.sourceNodes = require("./src/source-nodes");
exports.createResolvers = require("./src/create-resolvers");
-113
View File
@@ -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
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+9
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+20
View File
@@ -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

+19
View File
@@ -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"
}
-12581
View File
File diff suppressed because it is too large Load Diff
-12
View File
@@ -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"
}
+5 -5
View File
@@ -1,10 +1,10 @@
import { VideoTags } from "components/utils";
import { useContext } from "react"; import { useContext } from "react";
import Link from "next/link";
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
import { VideoTags } from "components/utils";
import PlayerContext from "context/playerContext"; import PlayerContext from "context/playerContext";
import createVideoSlug from "utils/createVideoSlug"; import createVideoSlug from "utils/createVideoSlug";
import { Link } from "gatsby";
import { Icon } from "components/icon"; import { Icon } from "components/icon";
import { faCompactDisc, faPlay } from "@fortawesome/free-solid-svg-icons";
import { Button } from "components/button"; import { Button } from "components/button";
export function VideoButton({ anime, theme, entry, video, ...props }) { export function VideoButton({ anime, theme, entry, video, ...props }) {
@@ -13,8 +13,8 @@ export function VideoButton({ anime, theme, entry, video, ...props }) {
const isPlaying = currentVideo && currentVideo.filename === video.filename; const isPlaying = currentVideo && currentVideo.filename === video.filename;
return ( return (
<Link to={`/anime/${anime.slug}/${videoSlug}`}> <Link href={`/anime/${anime.slug}/${videoSlug}`} passHref>
<Button {...props}> <Button as="a" {...props}>
<Button as="span" variant="primary"> <Button as="span" variant="primary">
<Icon icon={isPlaying ? faCompactDisc : faPlay} spin={isPlaying}/> <Icon icon={isPlaying ? faCompactDisc : faPlay} spin={isPlaying}/>
</Button> </Button>
+1 -1
View File
@@ -1,3 +1,3 @@
export { Button } from "./Button"; export { Button } from "./Button";
export { VideoButton } from "./VideoButton" export { VideoButton } from "./VideoButton";
export { FilterToggleButton } from "./FilterToggleButton"; export { FilterToggleButton } from "./FilterToggleButton";
+10 -38
View File
@@ -1,4 +1,4 @@
import { graphql, Link } from "gatsby"; import Link from "next/link";
import { faEllipsisH, faPlay } from "@fortawesome/free-solid-svg-icons"; import { faEllipsisH, faPlay } from "@fortawesome/free-solid-svg-icons";
import { Button } from "components/button"; import { Button } from "components/button";
import { Text } from "components/text"; import { Text } from "components/text";
@@ -7,7 +7,7 @@ import createVideoSlug from "utils/createVideoSlug";
import { Flex } from "components/box"; import { Flex } from "components/box";
import { Icon } from "components/icon"; import { Icon } from "components/icon";
import { SummaryCard } from "components/card"; import { SummaryCard } from "components/card";
import { chain, themeSequenceComparator, themeTypeComparator } from "utils/comparators"; import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) { export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
const { smallCover } = useImage(anime); const { smallCover } = useImage(anime);
@@ -25,8 +25,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
<SummaryCard.Description> <SummaryCard.Description>
<span>Anime</span> <span>Anime</span>
{!!anime.year && ( {!!anime.year && (
<Link to={premiereLink}> <Link href={premiereLink} passHref>
<Text link>{premiere}</Text> <Text as="a" link>{premiere}</Text>
</Link> </Link>
)} )}
<span>{anime.themes.length} themes</span> <span>{anime.themes.length} themes</span>
@@ -41,10 +41,10 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
to={animeLink} to={animeLink}
> >
{!hideThemes && ( {!hideThemes && (
<Flex display={[ "none", "flex" ]} flexWrap="wrap" gapsBoth="0.75rem"> <Flex display={["none", "flex"]} flexWrap="wrap" gapsBoth="0.75rem">
{anime.themes {anime.themes
.filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length) .filter((theme) => "entries" in theme && theme.entries.length && theme.entries[0].videos.length)
.sort(chain(themeTypeComparator, themeSequenceComparator)) .sort(chain(themeTypeComparator, themeIndexComparator))
.slice(0, maxThemes) .slice(0, maxThemes)
.map((theme) => { .map((theme) => {
const entry = theme.entries[0]; const entry = theme.entries[0];
@@ -52,8 +52,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
const videoSlug = createVideoSlug(theme, entry, video); const videoSlug = createVideoSlug(theme, entry, video);
return ( return (
<Link key={theme.slug} to={`/anime/${anime.slug}/${videoSlug}`}> <Link key={theme.slug} href={`/anime/${anime.slug}/${videoSlug}`} passHref>
<Button variant="on-card"> <Button as="a" variant="on-card">
<Button as="span" variant="primary"> <Button as="span" variant="primary">
<Icon icon={faPlay}/> <Icon icon={faPlay}/>
</Button> </Button>
@@ -64,8 +64,8 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
}) })
} }
{anime.themes.length > 4 && ( {anime.themes.length > 4 && (
<Link to={animeLink}> <Link href={animeLink} passHref>
<Button variant="on-card" title="Show all themes"> <Button as="a" variant="on-card" title="Show all themes">
<Icon icon={faEllipsisH}/> <Icon icon={faEllipsisH}/>
</Button> </Button>
</Link> </Link>
@@ -75,31 +75,3 @@ export function AnimeSummaryCard({ anime, hideThemes = false, maxThemes = 4 }) {
</SummaryCard> </SummaryCard>
); );
} }
export const query = graphql`
fragment AnimeCard on Anime {
name
slug
year
season
themes {
type
sequence
slug
}
resources {
site
link
}
images {
facet
link
}
}
fragment AnimeCardThemes on Anime {
themes {
...VideoSlug
}
}
`;
+35 -23
View File
@@ -1,53 +1,65 @@
import { Link, withPrefix } from "gatsby"; import Link from "next/link";
import { Text } from "components/text"; import { Text } from "components/text";
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import { Card } from "components/card"; import { Card } from "components/card";
import { Flex } from "components/box"; import { Flex } from "components/box";
import withBasePath from "utils/withBasePath";
const loadingAnimation = keyframes` const loadingAnimation = keyframes`
0% { background-position: 0 0; } 0% {
100% { background-position: 100% 100%; } background-position: 0 0;
}
100% {
background-position: 100% 100%;
}
`; `;
const StyledCover = styled.img.attrs({ const StyledCover = styled.img.attrs({
loading: "lazy" loading: "lazy"
})` })`
width: 48px; width: 48px;
height: 64px; height: 64px;
object-fit: cover; object-fit: cover;
background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat; background: radial-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.25)) no-repeat;
background-size: 500% 500%; background-size: 500% 500%;
@media (prefers-reduced-motion: no-preference) {
animation: ${loadingAnimation} 2s infinite alternate linear;
}
${(props) => props.placeholder && css` @media (prefers-reduced-motion: no-preference) {
padding: 0.5rem; animation: ${loadingAnimation} 2s infinite alternate linear;
object-fit: contain; }
background: white;
`} ${(props) => props.isPlaceholder && css`
padding: 0.5rem;
object-fit: contain;
background: white;
`}
`; `;
export function SummaryCard({ title, description, image, to, children, ...props }) { export function SummaryCard({ title, description, image, to, children, ...props }) {
return ( return (
<Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}> <Card display="flex" flexDirection="row" alignItems="center" p={0} pr="1rem" height="64px" {...props}>
<Link to={to}> <Link href={to}>
<StyledCover alt="Cover" src={image || withPrefix("/img/logo.svg")} placeholder={!image} loading="lazy"/> <a>
<StyledCover
alt="Cover"
src={image || withBasePath("/img/logo.svg")}
isPlaceholder={!image}
loading="lazy"
/>
</a>
</Link> </Link>
<Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem"> <Flex flex={1} flexDirection="column" justifyContent="center" gapsColumn="0.25rem" px="1rem">
<Text fontWeight="600" maxLines={1}> <Text fontWeight="600" maxLines={1}>
{typeof title === "string" ? ( {typeof title === "string" ? (
<Link to={to}> <Link href={to} passHref>
<Text link>{title}</Text> <Text as="a" link>{title}</Text>
</Link> </Link>
) : title} ) : title}
</Text> </Text>
<Text variant="small" maxLines={1}> <Text variant="small" maxLines={1}>
{typeof description === "string" ? ( {typeof description === "string" ? (
<SummaryCard.Description> <SummaryCard.Description>
{[ description ]} {[description]}
</SummaryCard.Description> </SummaryCard.Description>
) : description} ) : description}
</Text> </Text>
@@ -70,4 +82,4 @@ SummaryCard.Description = function SummaryCardDescription({ children }) {
))} ))}
</> </>
); );
} };
+26 -5
View File
@@ -1,9 +1,24 @@
import { Link } from "gatsby"; import Link from "next/link";
import { SongTitleWithArtists } from "components/utils"; import { SongTitleWithArtists } from "components/utils";
import { Text } from "components/text"; import { Text } from "components/text";
import useImage from "hooks/useImage"; import useImage from "hooks/useImage";
import createVideoSlug from "utils/createVideoSlug"; import createVideoSlug from "utils/createVideoSlug";
import { SummaryCard } from "components/card"; import { SummaryCard } from "components/card";
import { faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { Icon } from "components/icon";
import { Button } from "components/button";
import styled from "styled-components";
const StyledSummaryCard = styled(SummaryCard)``;
const StyledShowOnHover = styled.div`
display: none;
@media (hover: hover) and (pointer: fine) {
${StyledSummaryCard}:hover & {
display: initial;
}
}
`;
// Specify an artist if you want to display this in an artist context (e.g. artist page) // Specify an artist if you want to display this in an artist context (e.g. artist page)
export function ThemeSummaryCard({ theme, artist }) { export function ThemeSummaryCard({ theme, artist }) {
@@ -27,18 +42,24 @@ export function ThemeSummaryCard({ theme, artist }) {
<SummaryCard.Description> <SummaryCard.Description>
<span>Theme</span> <span>Theme</span>
<span>{theme.slug}</span> <span>{theme.slug}</span>
<Link to={`/anime/${theme.anime.slug}`}> <Link href={`/anime/${theme.anime.slug}`} passHref>
<Text link>{theme.anime.name}</Text> <Text as="a" link>{theme.anime.name}</Text>
</Link> </Link>
</SummaryCard.Description> </SummaryCard.Description>
); );
return ( return (
<SummaryCard <StyledSummaryCard
title={<SongTitleWithArtists song={theme.song} songTitleLinkTo={to} artist={artist}/>} title={<SongTitleWithArtists song={theme.song} songTitleLinkTo={to} artist={artist}/>}
description={description} description={description}
image={smallCover} image={smallCover}
to={to} to={to}
/> >
<StyledShowOnHover>
<Button variant="on-card" circle silent>
<Icon icon={faEllipsisV}/>
</Button>
</StyledShowOnHover>
</StyledSummaryCard>
); );
} }
+1 -1
View File
@@ -1,7 +1,7 @@
export { Card } from "./Card"; export { Card } from "./Card";
export { SummaryCard } from "./SummaryCard";
export { AnimeSummaryCard } from "./AnimeSummaryCard"; export { AnimeSummaryCard } from "./AnimeSummaryCard";
export { ThemeSummaryCard } from "./ThemeSummaryCard"; export { ThemeSummaryCard } from "./ThemeSummaryCard";
export { ArtistSummaryCard } from "./ArtistSummaryCard"; export { ArtistSummaryCard } from "./ArtistSummaryCard";
export { SummaryCard } from "./SummaryCard";
export { ThemeDetailCard } from "./ThemeDetailCard"; export { ThemeDetailCard } from "./ThemeDetailCard";
export { ErrorCard } from "./ErrorCard"; export { ErrorCard } from "./ErrorCard";
@@ -36,4 +36,4 @@ DescriptionList.Item = function DescriptionListItem({ title, children }) {
<StyledValue>{children}</StyledValue> <StyledValue>{children}</StyledValue>
</> </>
); );
} };
+2 -2
View File
@@ -3,7 +3,7 @@ import { Box, Flex } from "components/box";
import { ThemeDetailCard } from "components/card"; import { ThemeDetailCard } from "components/card";
import { Listbox } from "components/listbox"; import { Listbox } from "components/listbox";
import { HorizontalScroll } from "components/utils"; import { HorizontalScroll } from "components/utils";
import { chain, themeSequenceComparator, themeTypeComparator } from "utils/comparators"; import { chain, themeIndexComparator, themeTypeComparator } from "utils/comparators";
export function AnimeThemeFilter({ themes }) { export function AnimeThemeFilter({ themes }) {
const groups = useMemo(() => themes.reduce((groups, theme) => { const groups = useMemo(() => themes.reduce((groups, theme) => {
@@ -28,7 +28,7 @@ export function AnimeThemeFilter({ themes }) {
const filteredThemes = activeThemes const filteredThemes = activeThemes
.filter((theme) => !filterType || theme.type === filterType) .filter((theme) => !filterType || theme.type === filterType)
.sort(chain(themeTypeComparator, themeSequenceComparator)); .sort(chain(themeTypeComparator, themeIndexComparator));
return ( return (
<Box gapsColumn="1rem"> <Box gapsColumn="1rem">
+1 -1
View File
@@ -28,7 +28,7 @@ const StyledSearchInput = styled.div`
`; `;
const StyledForm = styled.form` const StyledForm = styled.form`
flex: 1; flex: 1;
` `;
const StyledInput = styled.input` const StyledInput = styled.input`
width: 100%; width: 100%;
`; `;
+19 -16
View File
@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { Link, withPrefix } from "gatsby"; import Link from "next/link";
import { import {
faBars, faBars,
faLightbulb, faLightbulb,
@@ -22,14 +22,15 @@ import ColorThemeContext from "context/colorThemeContext";
import { Icon } from "components/icon"; import { Icon } from "components/icon";
import { Text } from "components/text"; import { Text } from "components/text";
import { Flex } from "components/box"; import { Flex } from "components/box";
import { useLocation } from "@reach/router";
import useCurrentSeason from "hooks/useCurrentSeason"; import useCurrentSeason from "hooks/useCurrentSeason";
import navigateToRandomTheme from "utils/navigateToRandomTheme"; import navigateToRandomTheme from "utils/navigateToRandomTheme";
import { useRouter } from "next/router";
import withBasePath from "utils/withBasePath";
export function Navigation({ offsetToggleButton = false }) { export function Navigation({ offsetToggleButton = false }) {
const [ show, setShow ] = useState(false); const [ show, setShow ] = useState(false);
const { colorTheme, toggleColorTheme } = useContext(ColorThemeContext); const { colorTheme, toggleColorTheme } = useContext(ColorThemeContext);
const location = useLocation(); const router = useRouter();
const { currentYear, currentSeason } = useCurrentSeason(); const { currentYear, currentSeason } = useCurrentSeason();
@@ -37,28 +38,30 @@ export function Navigation({ offsetToggleButton = false }) {
// we want to close the modal navigation. // we want to close the modal navigation.
useEffect(() => { useEffect(() => {
setShow(false); setShow(false);
}, [location]); }, [router.pathname]);
return ( return (
<> <>
<StyledNavigation show={show} onClick={() => setShow(false)}> <StyledNavigation show={show} onClick={() => setShow(false)}>
<StyledNavigationContainer onClick={(event) => event.stopPropagation()}> <StyledNavigationContainer onClick={(event) => event.stopPropagation()}>
<StyledLogoContainer to="/"> <Link href="/" passHref>
<StyledLogo <StyledLogoContainer>
src={withPrefix("/img/logo.svg")} <StyledLogo
alt="Logo" src={withBasePath("/img/logo.svg")}
width="277" alt="Logo"
height="150" width="277"
/> height="150"
</StyledLogoContainer> />
</StyledLogoContainer>
</Link>
<Flex <Flex
flexDirection={[ "column", "row" ]} flexDirection={[ "column", "row" ]}
gapsRow={[ 0, "0.5rem" ]} gapsRow={[ 0, "0.5rem" ]}
gapsColumn={[ "0.5rem", 0 ]} gapsColumn={[ "0.5rem", 0 ]}
alignItems="flex-start" alignItems="flex-start"
> >
<Link to="/search"> <Link href="/search" passHref>
<Button variant="on-card" silent gapsRow="0.5rem"> <Button as="a" variant="on-card" silent gapsRow="0.5rem">
<Icon icon={faSearch}/> <Icon icon={faSearch}/>
<Text>Search</Text> <Text>Search</Text>
</Button> </Button>
@@ -67,8 +70,8 @@ export function Navigation({ offsetToggleButton = false }) {
<Icon icon={faRandom}/> <Icon icon={faRandom}/>
<Text>Play Random</Text> <Text>Play Random</Text>
</Button> </Button>
<Link to={`/year/${currentYear}/${currentSeason}`}> <Link href={(currentYear && currentSeason) ? `/year/${currentYear}/${currentSeason}` : "/"} passHref>
<Button variant="on-card" silent gapsRow="0.5rem"> <Button as="a" variant="on-card" silent gapsRow="0.5rem">
<Icon icon={faTv}/> <Icon icon={faTv}/>
<Text>Current Season</Text> <Text>Current Season</Text>
</Button> </Button>
@@ -1,7 +1,6 @@
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { Container } from "components/container"; import { Container } from "components/container";
import { gapsColumn, gapsRow } from "styles/mixins"; import { gapsColumn, gapsRow } from "styles/mixins";
import { Link } from "gatsby";
import { Button } from "components/button"; import { Button } from "components/button";
import theme from "theme"; import theme from "theme";
@@ -59,7 +58,7 @@ export const StyledNavigationContainer = styled(Container)`
} }
`; `;
export const StyledLogoContainer = styled(Link)` export const StyledLogoContainer = styled.a`
display: flex; display: flex;
align-items: center; align-items: center;
+28 -24
View File
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useRef } from "react";
import { Link, navigate } from "gatsby"; import Link from "next/link";
import { Box, Flex } from "components/box"; import { Box, Flex } from "components/box";
import { SEO } from "components/seo";
import { Text } from "components/text"; import { Text } from "components/text";
import { SearchInput } from "components/input"; import { SearchInput } from "components/input";
import { HorizontalScroll } from "components/utils"; import { HorizontalScroll } from "components/utils";
@@ -10,6 +9,7 @@ import { Icon } from "components/icon";
import { faTimes } from "@fortawesome/free-solid-svg-icons"; import { faTimes } from "@fortawesome/free-solid-svg-icons";
import styled from "styled-components"; import styled from "styled-components";
import theme from "theme"; import theme from "theme";
import { useRouter } from "next/router";
const StyledSearchOptions = styled(Flex)` const StyledSearchOptions = styled(Flex)`
flex-wrap: wrap; flex-wrap: wrap;
@@ -20,36 +20,41 @@ const StyledSearchOptions = styled(Flex)`
} }
`; `;
export function SearchNavigation({ entity, location: { search, hash, state } }) { export function SearchNavigation() {
const urlParams = useMemo(() => new URLSearchParams(search), [ search ]); const router = useRouter();
const urlSuffix = search + hash; const { entity, ...urlParams } = router.query;
const searchQuery = urlParams.q || "";
const [ searchQuery, setSearchQuery ] = useState(urlParams.get("q") || "");
// Generates page title based on search query // Generates page title based on search query
const pageTitle = searchQuery && searchQuery.trim() const pageTitle = searchQuery && searchQuery.trim()
? `${searchQuery} - Search` ? `${searchQuery} - Search`
: "Search"; : "Search";
useEffect(() => { const updateSearchQuery = (newSearchQuery) => {
// Update URL to maintain the searchQuery on page navigation. // Update URL to maintain the searchQuery on page navigation.
const newUrlParams = new URLSearchParams(); const newUrlParams = {
if (searchQuery) { ...urlParams
newUrlParams.set("q", searchQuery); };
if (newSearchQuery) {
newUrlParams.q = newSearchQuery;
} else {
delete newUrlParams.q;
} }
const params = newUrlParams.toString();
let url = "/search"; let url = "/search";
if (entity) { if (entity) {
url += `/${entity}`; url += `/${entity}`;
} }
if (params) {
url += `?${params}`;
}
navigate(url, { replace: true, state }); router.replace({
pathname: url,
query: newUrlParams
}, null, {
shallow: true
});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ entity, searchQuery ]); };
const inputRef = useRef(); const inputRef = useRef();
@@ -64,13 +69,12 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
return ( return (
<Box gapsColumn="1.5rem"> <Box gapsColumn="1.5rem">
<SEO title={pageTitle} />
<Text variant="h1">Search</Text> <Text variant="h1">Search</Text>
<StyledSearchOptions> <StyledSearchOptions>
<Box flex="1" minWidth="33%"> <Box flex="1" minWidth="33%">
<SearchInput <SearchInput
query={searchQuery} query={searchQuery}
setQuery={setSearchQuery} setQuery={updateSearchQuery}
inputProps={{ inputProps={{
ref: inputRef, ref: inputRef,
spellCheck: false spellCheck: false
@@ -84,15 +88,15 @@ export function SearchNavigation({ entity, location: { search, hash, state } })
> >
{({ Button, item, selected, content }) => item.value === null ? ( {({ Button, item, selected, content }) => item.value === null ? (
!!entity ? ( !!entity ? (
<Link key={null} to={`/search${urlSuffix}`}> <Link key={null} href={{ pathname: "/search", query: urlParams }} passHref>
<Button> <Button as="a">
<Icon icon={faTimes}/> <Icon icon={faTimes}/>
</Button> </Button>
</Link> </Link>
) : null ) : null
) : ( ) : (
<Link key={item.value} to={`/search/${item.value}${urlSuffix}`}> <Link key={item.value} href={{ pathname: `/search/${item.value}`, query: urlParams }} passHref>
<Button variant={selected && "primary"}> <Button as="a" variant={selected && "primary"}>
{content} {content}
</Button> </Button>
</Link> </Link>
@@ -1,6 +1,6 @@
import { Box } from "components/box"; import { Box } from "components/box";
import { Switcher } from "components/switcher"; import { Switcher } from "components/switcher";
import { Link } from "gatsby"; import Link from "next/link";
export function SeasonNavigation({ year, season, seasonList }) { export function SeasonNavigation({ year, season, seasonList }) {
return ( return (
@@ -11,8 +11,8 @@ export function SeasonNavigation({ year, season, seasonList }) {
mx="auto" mx="auto"
> >
{({ Button, item, selected, content }) => ( {({ Button, item, selected, content }) => (
<Link key={item.value} to={`/year/${year}/${item.value}`}> <Link key={item.value} href={`/year/${year}/${item.value}`} passHref>
<Button variant={selected && "primary"}> <Button as="a" variant={selected && "primary"}>
{content} {content}
</Button> </Button>
</Link> </Link>
+8 -18
View File
@@ -1,4 +1,4 @@
import { graphql, Link, useStaticQuery } from "gatsby"; import Link from "next/link";
import { Button } from "components/button"; import { Button } from "components/button";
import styled from "styled-components"; import styled from "styled-components";
import { Flex } from "components/box"; import { Flex } from "components/box";
@@ -18,17 +18,7 @@ const StyledYearNext = styled(StyledYear)`
justify-content: flex-start; justify-content: flex-start;
`; `;
export function YearNavigation({ year }) { export function YearNavigation({ year, yearList }) {
const yearList = useStaticQuery(graphql`
query {
allAnime {
groupedByYear: group(field: year) {
year: fieldValue
}
}
}
`).allAnime.groupedByYear.map((group) => +group.year);
const previousYear = yearList.indexOf(year) > 0 ? yearList[yearList.indexOf(year) - 1] : null; const previousYear = yearList.indexOf(year) > 0 ? yearList[yearList.indexOf(year) - 1] : null;
const nextYear = yearList.indexOf(year) < yearList.length - 1 ? yearList[yearList.indexOf(year) + 1] : null; const nextYear = yearList.indexOf(year) < yearList.length - 1 ? yearList[yearList.indexOf(year) + 1] : null;
@@ -36,20 +26,20 @@ export function YearNavigation({ year }) {
<Flex alignItems="center"> <Flex alignItems="center">
<StyledYearPrevious> <StyledYearPrevious>
{previousYear && ( {previousYear && (
<Link to={`/year/${previousYear}`}> <Link href={`/year/${previousYear}`} passHref>
<Button silent>{previousYear}</Button> <Button as="a" silent>{previousYear}</Button>
</Link> </Link>
)} )}
</StyledYearPrevious> </StyledYearPrevious>
<Link to={`/year`}> <Link href={`/year`} passHref>
<Button silent> <Button as="a" silent>
<Text variant="h1">{year}</Text> <Text variant="h1">{year}</Text>
</Button> </Button>
</Link> </Link>
<StyledYearNext> <StyledYearNext>
{nextYear && ( {nextYear && (
<Link to={`/year/${nextYear}`}> <Link href={`/year/${nextYear}`} passHref>
<Button silent>{nextYear}</Button> <Button as="a" silent>{nextYear}</Button>
</Link> </Link>
)} )}
</StyledYearNext> </StyledYearNext>
@@ -1,21 +1,9 @@
import { Text } from "components/text"; import { Text } from "components/text";
import { Listbox } from "components/listbox"; import { Listbox } from "components/listbox";
import { Flex } from "components/box"; import { Flex } from "components/box";
import { graphql, useStaticQuery } from "gatsby";
export function SearchFilterYear({ value, setValue }) { export function SearchFilterYear({ value, setValue }) {
const yearList = useStaticQuery(graphql` const yearList = [ 2021, 2020, 2019 ];
query {
allAnime {
groupedByYear: group(field: year) {
year: fieldValue
}
}
}
`).allAnime.groupedByYear
.map((node) => node.year)
.sort()
.reverse();
return ( return (
<Flex flexDirection="column" alignItems="stretch" gapsColumn="0.5rem"> <Flex flexDirection="column" alignItems="stretch" gapsColumn="0.5rem">
+18 -24
View File
@@ -6,8 +6,8 @@ import {
} from "components/search-filter"; } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch"; import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search"; import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { AnimeSummaryCard } from "components/card"; import { AnimeSummaryCard } from "components/card";
import useSessionStorage from "hooks/useSessionStorage";
const sortByFields = new Map([ const sortByFields = new Map([
[ "A ➜ Z", "name" ], [ "A ➜ Z", "name" ],
@@ -18,29 +18,23 @@ const sortByFields = new Map([
]); ]);
const sortByOptions = [ ...sortByFields.keys() ]; const sortByOptions = [ ...sortByFields.keys() ];
export function SearchAnime({ searchQuery, locationState }) { const initialFilter = {
const filterFirstLetter = locationState?.filterFirstLetter || null; firstLetter: null,
const filterSeason = locationState?.filterSeason || null; season: null,
const filterYear = locationState?.filterYear || null; year: null,
const sortBy = locationState?.sortBy || sortByOptions[0]; sortBy: sortByOptions[0]
};
const updateState = (field) => (newValue) => { export function SearchAnime({ searchQuery }) {
navigate("", { const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const entitySearch = useEntitySearch("anime", searchQuery, { const entitySearch = useEntitySearch("anime", searchQuery, {
filters: { filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null, "name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
season: filterSeason, season: filter.season,
year: filterYear year: filter.year
}, },
sortBy: searchQuery ? null : sortByFields.get(sortBy) sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
}); });
return ( return (
@@ -48,13 +42,13 @@ export function SearchAnime({ searchQuery, locationState }) {
searchQuery={searchQuery} searchQuery={searchQuery}
filters={ filters={
<> <>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/> <SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
<SearchFilterSeason value={filterSeason} setValue={updateState("filterSeason")}/> <SearchFilterSeason value={filter.season} setValue={updateFilter("season")}/>
<SearchFilterYear value={filterYear} setValue={updateState("filterYear")}/> <SearchFilterYear value={filter.year} setValue={updateFilter("year")}/>
<SearchFilterSortBy <SearchFilterSortBy
options={searchQuery ? [ "Relevance" ] : sortByOptions} options={searchQuery ? [ "Relevance" ] : sortByOptions}
value={searchQuery ? "Relevance" : sortBy} value={searchQuery ? "Relevance" : filter.sortBy}
setValue={updateState("sortBy")} setValue={updateFilter("sortBy")}
/> />
</> </>
} }
+12 -18
View File
@@ -1,8 +1,8 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter"; import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch"; import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search"; import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { ArtistSummaryCard } from "components/card"; import { ArtistSummaryCard } from "components/card";
import useSessionStorage from "hooks/useSessionStorage";
const sortByFields = new Map([ const sortByFields = new Map([
[ "A ➜ Z", "name" ], [ "A ➜ Z", "name" ],
@@ -11,25 +11,19 @@ const sortByFields = new Map([
]); ]);
const sortByOptions = [ ...sortByFields.keys() ]; const sortByOptions = [ ...sortByFields.keys() ];
export function SearchArtist({ searchQuery, locationState }) { const initialFilter = {
const filterFirstLetter = locationState?.filterFirstLetter || null; firstLetter: null,
const sortBy = locationState?.sortBy || sortByOptions[0]; sortBy: sortByOptions[0]
};
const updateState = (field) => (newValue) => { export function SearchArtist({ searchQuery }) {
navigate("", { const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const entitySearch = useEntitySearch("artist", searchQuery, { const entitySearch = useEntitySearch("artist", searchQuery, {
filters: { filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null, "name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
}, },
sortBy: searchQuery ? null : sortByFields.get(sortBy) sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
}); });
return ( return (
@@ -37,11 +31,11 @@ export function SearchArtist({ searchQuery, locationState }) {
searchQuery={searchQuery} searchQuery={searchQuery}
filters={ filters={
<> <>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/> <SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
<SearchFilterSortBy <SearchFilterSortBy
options={searchQuery ? [ "Relevance" ] : sortByOptions} options={searchQuery ? [ "Relevance" ] : sortByOptions}
value={searchQuery ? "Relevance" : sortBy} value={searchQuery ? "Relevance" : filter.sortBy}
setValue={updateState("sortBy")} setValue={updateFilter("sortBy")}
/> />
</> </>
} }
+13 -11
View File
@@ -1,19 +1,19 @@
import { useLocation } from "@reach/router"; import Link from "next/link";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { Button } from "components/button"; import { Button } from "components/button";
import { Text } from "components/text"; import { Text } from "components/text";
import { Box, Flex } from "components/box"; import { Box, Flex } from "components/box";
import { Link } from "gatsby";
import { Icon } from "components/icon"; import { Icon } from "components/icon";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { fetchGlobalSearchResults } from "lib/search"; import { fetchGlobalSearchResults } from "lib/client/search";
import { AnimeSummaryCard, ArtistSummaryCard, ErrorCard, SummaryCard, ThemeSummaryCard } from "components/card"; import { AnimeSummaryCard, ArtistSummaryCard, ErrorCard, SummaryCard, ThemeSummaryCard } from "components/card";
import { useRouter } from "next/router";
export function SearchGlobal({ searchQuery }) { export function SearchGlobal({ searchQuery }) {
const fetchSearchResults = () => fetchGlobalSearchResults( const fetchSearchResults = () => fetchGlobalSearchResults(
searchQuery, searchQuery,
4, 4,
[ "anime", "theme", "artist", "series", "studio" ] ["anime", "theme", "artist", "series", "studio"]
); );
const { const {
@@ -22,7 +22,7 @@ export function SearchGlobal({ searchQuery }) {
isLoading, isLoading,
isError isError
} = useQuery( } = useQuery(
[ "searchGlobal", searchQuery ], ["searchGlobal", searchQuery],
fetchSearchResults, fetchSearchResults,
{ {
keepPreviousData: true keepPreviousData: true
@@ -87,7 +87,8 @@ export function SearchGlobal({ searchQuery }) {
title="Series" title="Series"
results={seriesResults} results={seriesResults}
renderSummaryCard={(series) => ( renderSummaryCard={(series) => (
<SummaryCard key={series.slug} title={series.name} description="Series" to={`/series/${series.slug}`} /> <SummaryCard key={series.slug} title={series.name} description="Series"
to={`/series/${series.slug}`}/>
)} )}
/> />
<GlobalSearchSection <GlobalSearchSection
@@ -95,7 +96,8 @@ export function SearchGlobal({ searchQuery }) {
title="Studios" title="Studios"
results={studioResults} results={studioResults}
renderSummaryCard={(studio) => ( renderSummaryCard={(studio) => (
<SummaryCard key={studio.slug} title={studio.name} description="Studio" to={`/studio/${studio.slug}`} /> <SummaryCard key={studio.slug} title={studio.name} description="Studio"
to={`/studio/${studio.slug}`}/>
)} )}
/> />
</Box> </Box>
@@ -103,8 +105,8 @@ export function SearchGlobal({ searchQuery }) {
} }
function GlobalSearchSection({ entity, title, results, renderSummaryCard }) { function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
const { search, hash } = useLocation(); const router = useRouter();
const urlSuffix = search + hash; const urlParams = router.query;
if (!results.length) { if (!results.length) {
return null; return null;
@@ -121,8 +123,8 @@ function GlobalSearchSection({ entity, title, results, renderSummaryCard }) {
</Box> </Box>
{hasMoreResults && ( {hasMoreResults && (
<Flex justifyContent="center"> <Flex justifyContent="center">
<Link to={`/search/${entity}${urlSuffix}`}> <Link href={{ pathname: `/search/${entity}`, query: urlParams }} passHref>
<Button silent title="See all results"> <Button as="a" silent title="See all results">
<Icon icon={faChevronDown}/> <Icon icon={faChevronDown}/>
</Button> </Button>
</Link> </Link>
+12 -18
View File
@@ -1,8 +1,8 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter"; import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch"; import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search"; import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { SummaryCard } from "components/card"; import { SummaryCard } from "components/card";
import useSessionStorage from "hooks/useSessionStorage";
const sortByFields = new Map([ const sortByFields = new Map([
[ "A ➜ Z", "name" ], [ "A ➜ Z", "name" ],
@@ -11,25 +11,19 @@ const sortByFields = new Map([
]); ]);
const sortByOptions = [ ...sortByFields.keys() ]; const sortByOptions = [ ...sortByFields.keys() ];
export function SearchSeries({ searchQuery, locationState }) { const initialFilter = {
const filterFirstLetter = locationState?.filterFirstLetter || null; firstLetter: null,
const sortBy = locationState?.sortBy || sortByOptions[0]; sortBy: sortByOptions[0]
};
const updateState = (field) => (newValue) => { export function SearchSeries({ searchQuery }) {
navigate("", { const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const entitySearch = useEntitySearch("series", searchQuery, { const entitySearch = useEntitySearch("series", searchQuery, {
filters: { filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null, "name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
}, },
sortBy: searchQuery ? null : sortByFields.get(sortBy) sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
}); });
return ( return (
@@ -37,11 +31,11 @@ export function SearchSeries({ searchQuery, locationState }) {
searchQuery={searchQuery} searchQuery={searchQuery}
filters={ filters={
<> <>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/> <SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
<SearchFilterSortBy <SearchFilterSortBy
options={searchQuery ? [ "Relevance" ] : sortByOptions} options={searchQuery ? [ "Relevance" ] : sortByOptions}
value={searchQuery ? "Relevance" : sortBy} value={searchQuery ? "Relevance" : filter.sortBy}
setValue={updateState("sortBy")} setValue={updateFilter("sortBy")}
/> />
</> </>
} }
+12 -18
View File
@@ -1,8 +1,8 @@
import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter"; import { SearchFilterFirstLetter, SearchFilterSortBy } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch"; import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search"; import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { SummaryCard } from "components/card"; import { SummaryCard } from "components/card";
import useSessionStorage from "hooks/useSessionStorage";
const sortByFields = new Map([ const sortByFields = new Map([
[ "A ➜ Z", "name" ], [ "A ➜ Z", "name" ],
@@ -11,25 +11,19 @@ const sortByFields = new Map([
]); ]);
const sortByOptions = [ ...sortByFields.keys() ]; const sortByOptions = [ ...sortByFields.keys() ];
export function SearchStudio({ searchQuery, locationState }) { const initialFilter = {
const filterFirstLetter = locationState?.filterFirstLetter || null; firstLetter: null,
const sortBy = locationState?.sortBy || sortByOptions[0]; sortBy: sortByOptions[0]
};
const updateState = (field) => (newValue) => { export function SearchStudio({ searchQuery }) {
navigate("", { const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const entitySearch = useEntitySearch("studio", searchQuery, { const entitySearch = useEntitySearch("studio", searchQuery, {
filters: { filters: {
"name][like": filterFirstLetter ? `${filterFirstLetter}%` : null, "name][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
}, },
sortBy: searchQuery ? null : sortByFields.get(sortBy) sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
}); });
return ( return (
@@ -37,11 +31,11 @@ export function SearchStudio({ searchQuery, locationState }) {
searchQuery={searchQuery} searchQuery={searchQuery}
filters={ filters={
<> <>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/> <SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
<SearchFilterSortBy <SearchFilterSortBy
options={searchQuery ? [ "Relevance" ] : sortByOptions} options={searchQuery ? [ "Relevance" ] : sortByOptions}
value={searchQuery ? "Relevance" : sortBy} value={searchQuery ? "Relevance" : filter.sortBy}
setValue={updateState("sortBy")} setValue={updateFilter("sortBy")}
/> />
</> </>
} }
+15 -21
View File
@@ -1,8 +1,8 @@
import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter"; import { SearchFilterFirstLetter, SearchFilterSortBy, SearchFilterThemeType } from "components/search-filter";
import useEntitySearch from "hooks/useEntitySearch"; import useEntitySearch from "hooks/useEntitySearch";
import { SearchEntity } from "components/search"; import { SearchEntity } from "components/search";
import { navigate } from "gatsby";
import { ThemeSummaryCard } from "components/card"; import { ThemeSummaryCard } from "components/card";
import useSessionStorage from "hooks/useSessionStorage";
const sortByFields = new Map([ const sortByFields = new Map([
[ "A ➜ Z", "song.title" ], [ "A ➜ Z", "song.title" ],
@@ -13,28 +13,22 @@ const sortByFields = new Map([
]); ]);
const sortByOptions = [ ...sortByFields.keys() ]; const sortByOptions = [ ...sortByFields.keys() ];
export function SearchTheme({ searchQuery, locationState }) { const initialFilter = {
const filterFirstLetter = locationState?.filterFirstLetter || null; firstLetter: null,
const filterType = locationState?.filterType || null; type: null,
const sortBy = locationState?.sortBy || sortByOptions[0]; sortBy: sortByOptions[0]
};
const updateState = (field) => (newValue) => { export function SearchTheme({ searchQuery }) {
navigate("", { const { updateDataField: updateFilter, data: filter } = useSessionStorage("filter", initialFilter);
state: {
...locationState,
[field]: newValue
},
replace: true
});
};
const entitySearch = useEntitySearch("theme", searchQuery, { const entitySearch = useEntitySearch("theme", searchQuery, {
filters: { filters: {
has: "song", has: "song",
"song][title][like": filterFirstLetter ? `${filterFirstLetter}%` : null, "song][title][like": filter.firstLetter ? `${filter.firstLetter}%` : null,
type: filterType type: filter.type
}, },
sortBy: searchQuery ? null : sortByFields.get(sortBy) sortBy: searchQuery ? null : sortByFields.get(filter.sortBy)
}); });
return ( return (
@@ -42,12 +36,12 @@ export function SearchTheme({ searchQuery, locationState }) {
searchQuery={searchQuery} searchQuery={searchQuery}
filters={ filters={
<> <>
<SearchFilterFirstLetter value={filterFirstLetter} setValue={updateState("filterFirstLetter")}/> <SearchFilterFirstLetter value={filter.firstLetter} setValue={updateFilter("firstLetter")}/>
<SearchFilterThemeType value={filterType} setValue={updateState("filterType")}/> <SearchFilterThemeType value={filter.type} setValue={updateFilter("type")}/>
<SearchFilterSortBy <SearchFilterSortBy
options={searchQuery ? [ "Relevance" ] : sortByOptions} options={searchQuery ? [ "Relevance" ] : sortByOptions}
value={searchQuery ? "Relevance" : sortBy} value={searchQuery ? "Relevance" : filter.sortBy}
setValue={updateState("sortBy")} setValue={updateFilter("sortBy")}
/> />
</> </>
} }
+19 -55
View File
@@ -1,59 +1,23 @@
import PropTypes from "prop-types"; import Head from "next/head";
import { useLocation } from "@reach/router"; import withBasePath from "utils/withBasePath";
import { Helmet } from "react-helmet";
import useSiteMeta from "hooks/useSiteMeta";
export function SEO({ title, description, meta = [], lang }) { export function SEO({
const { pathname } = useLocation(); title,
description = "AnimeThemes is a simple and consistent repository of anime opening and ending themes.",
const { image = withBasePath("/img/logo.svg"),
description: defaultDescription, children
lang: defaultLanguage, }) {
titleTemplate, const titleWithSuffix = title ? `${title} · AnimeThemes` : "AnimeThemes";
siteName,
siteUrl
} = useSiteMeta();
const seo = {
url: `${siteUrl}${pathname}`,
lang: lang || defaultLanguage,
title: title || siteName,
description: description || defaultDescription
}
return ( return (
<Helmet <Head>
htmlAttributes={{ <title>{titleWithSuffix}</title>
lang: seo.lang <meta property="description" content={description}/>
}} <meta property="og:title" content={titleWithSuffix}/>
<meta property="og:description" content={description}/>
title={seo.title} <meta property="og:image" content={image}/>
titleTemplate={title ? titleTemplate : null} <meta property="og:site_name" content="AnimeThemes"/>
meta={[ {children}
{ </Head>
name: "description", );
content: seo.description
},
{
property: "og:site_name",
content: siteName
},
{
property: "og:title",
content: title ? titleTemplate.replace(/%s/g, title) : seo.title
},
{
property: "og:description",
content: seo.description
}
].concat(meta)}
/>
)
}
SEO.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
lang: PropTypes.string
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import styled from "styled-components"; import styled from "styled-components";
import { AnimateSharedLayout, motion } from "framer-motion"; import { AnimateSharedLayout, m } from "framer-motion";
import { Box } from "components/box"; import { Box } from "components/box";
import { Button } from "components/button"; import { Button } from "components/button";
import theme from "theme"; import theme from "theme";
@@ -26,7 +26,7 @@ const StyledButton = styled(Button)`
background-color: transparent; background-color: transparent;
} }
`; `;
const StyledButtonBackground = styled(motion.div)` const StyledButtonBackground = styled(m.div)`
position: absolute; position: absolute;
z-index: ${theme.zIndices.switcherButton}; z-index: ${theme.zIndices.switcherButton};
top: 0; top: 0;
+3 -3
View File
@@ -1,4 +1,4 @@
import { motion } from "framer-motion"; import { m } from "framer-motion";
export function Collapse({ collapse, children }) { export function Collapse({ collapse, children }) {
if (collapse) { if (collapse) {
@@ -6,12 +6,12 @@ export function Collapse({ collapse, children }) {
} }
return ( return (
<motion.div <m.div
initial={{ y: -20, opacity: 0 }} initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ type: "tween", duration: 0.35, ease: [ 0, 0.66, 0.46, 0.98 ] }} transition={{ type: "tween", duration: 0.35, ease: [ 0, 0.66, 0.46, 0.98 ] }}
> >
{children} {children}
</motion.div> </m.div>
); );
} }
+9 -9
View File
@@ -1,5 +1,5 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { Link } from "gatsby"; import Link from "next/link";
import { Text } from "components/text"; import { Text } from "components/text";
// Specify an artist if you want to display this in an artist context (e.g. artist page) // Specify an artist if you want to display this in an artist context (e.g. artist page)
@@ -10,8 +10,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
<Text> <Text>
{songTitleLinkTo {songTitleLinkTo
? ( ? (
<Link to={songTitleLinkTo}> <Link href={songTitleLinkTo} passHref>
<Text link italics={!song.title}>{songTitle}</Text> <Text as="a" link italics={!song.title}>{songTitle}</Text>
</Link> </Link>
) )
: ( : (
@@ -27,8 +27,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
{!!performedAs.as && ( {!!performedAs.as && (
<> <>
<Text variant="small" color="text-muted"> as </Text> <Text variant="small" color="text-muted"> as </Text>
<Link to={`/artist/${performedAs.artist.slug}`}> <Link href={`/artist/${performedAs.artist.slug}`} passHref>
<Text link> <Text as="a" link>
{performedAs.as} {performedAs.as}
</Text> </Text>
</Link> </Link>
@@ -39,8 +39,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
<Text variant="small" color="text-muted"> with </Text> <Text variant="small" color="text-muted"> with </Text>
{performedWith.map((performance, index) => ( {performedWith.map((performance, index) => (
<Fragment key={performance.artist.slug}> <Fragment key={performance.artist.slug}>
<Link to={`/artist/${performance.artist.slug}`}> <Link href={`/artist/${performance.artist.slug}`} passHref>
<Text link> <Text as="a" link>
{performance.as || performance.artist.name} {performance.as || performance.artist.name}
</Text> </Text>
</Link> </Link>
@@ -60,8 +60,8 @@ export function SongTitleWithArtists({ song, songTitleLinkTo, artist }) {
<Text variant="small" color="text-muted"> by </Text> <Text variant="small" color="text-muted"> by </Text>
{song.performances.map((performance, index) => ( {song.performances.map((performance, index) => (
<Fragment key={performance.artist.slug}> <Fragment key={performance.artist.slug}>
<Link to={`/artist/${performance.artist.slug}`}> <Link href={`/artist/${performance.artist.slug}`} passHref>
<Text link> <Text as="a" link>
{performance.as || performance.artist.name} {performance.as || performance.artist.name}
</Text> </Text>
</Link> </Link>
+1 -1
View File
@@ -1,7 +1,7 @@
export { AspectRatio } from "./AspectRatio"; export { AspectRatio } from "./AspectRatio";
export { Collapse } from "./Collapse"; export { Collapse } from "./Collapse";
export { HeightTransition } from "./HeightTransition"; export { HeightTransition } from "./HeightTransition";
export { SongTitleWithArtists } from "./SongTitleWithArtists" export { SongTitleWithArtists } from "./SongTitleWithArtists";
export { ThemeEntryTags } from "./ThemeEntryTags"; export { ThemeEntryTags } from "./ThemeEntryTags";
export { VideoTags } from "./VideoTags"; export { VideoTags } from "./VideoTags";
export { HorizontalScroll } from "./HorizontalScroll"; export { HorizontalScroll } from "./HorizontalScroll";
+4 -3
View File
@@ -6,7 +6,6 @@ import {
StyledOverlay, StyledPlayerInfo, StyledPlayerProgress, StyledPlayerProgressBar StyledOverlay, StyledPlayerInfo, StyledPlayerProgress, StyledPlayerProgressBar
} from "./VideoPlayer.style"; } from "./VideoPlayer.style";
import { faDownload, faExpandAlt, faPause, faPlay, faTimes } from "@fortawesome/free-solid-svg-icons"; import { faDownload, faExpandAlt, faPause, faPlay, faTimes } from "@fortawesome/free-solid-svg-icons";
import { navigate } from "gatsby";
import PlayerContext from "context/playerContext"; import PlayerContext from "context/playerContext";
import createVideoSlug from "utils/createVideoSlug"; import createVideoSlug from "utils/createVideoSlug";
import { useMedia } from "use-media"; import { useMedia } from "use-media";
@@ -17,8 +16,9 @@ import { Text } from "components/text";
import { Flex } from "components/box"; import { Flex } from "components/box";
import { Container } from "components/container"; import { Container } from "components/container";
import useCompatability from "hooks/useCompatability"; import useCompatability from "hooks/useCompatability";
import { useRouter } from "next/router";
const videoBaseUrl = process.env.GATSBY_VIDEO_URL || "https://animethemes.moe"; const videoBaseUrl = process.env.NEXT_PUBLIC_VIDEO_URL || "https://animethemes.moe";
export function VideoPlayer({ video, entry, background, ...props }) { export function VideoPlayer({ video, entry, background, ...props }) {
const [isPlaying, setPlaying] = useState(false); const [isPlaying, setPlaying] = useState(false);
@@ -28,6 +28,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
const { setCurrentVideo } = useContext(PlayerContext); const { setCurrentVideo } = useContext(PlayerContext);
const isMobile = useMedia({ maxWidth: "720px" }); const isMobile = useMedia({ maxWidth: "720px" });
const videoUrl = `${videoBaseUrl}/video/${video.basename}`; const videoUrl = `${videoBaseUrl}/video/${video.basename}`;
const router = useRouter();
function togglePlay() { function togglePlay() {
if (isPlaying) { if (isPlaying) {
@@ -39,7 +40,7 @@ export function VideoPlayer({ video, entry, background, ...props }) {
function maximize() { function maximize() {
const videoSlug = createVideoSlug(entry.theme, entry, video); const videoSlug = createVideoSlug(entry.theme, entry, video);
navigate(`/anime/${entry.theme.anime.slug}/${videoSlug}`); router.push(`/anime/${entry.theme.anime.slug}/${videoSlug}`);
} }
function preventTextSelection(event) { function preventTextSelection(event) {
@@ -1,5 +1,5 @@
import styled, { css, keyframes } from "styled-components"; import styled, { css, keyframes } from "styled-components";
import { motion } from "framer-motion"; import { m } from "framer-motion";
import { gapsColumn, gapsRow } from "styles/mixins"; import { gapsColumn, gapsRow } from "styles/mixins";
import { Button } from "components/button"; import { Button } from "components/button";
import theme from "theme"; import theme from "theme";
@@ -14,7 +14,7 @@ const slideIn = keyframes`
} }
`; `;
export const StyledPlayer = styled(motion.div)` export const StyledPlayer = styled(m.div)`
${(props) => props.background ? css` ${(props) => props.background ? css`
position: fixed; position: fixed;
z-index: ${theme.zIndices.videoPlayer}; z-index: ${theme.zIndices.videoPlayer};
+20 -23
View File
@@ -1,29 +1,26 @@
import { graphql, useStaticQuery } from "gatsby"; import { useEffect, useState } from "react";
import { fetchCurrentSeason } from "lib/client/currentSeason";
const seasonOrder = [ let cache = null;
"winter",
"spring",
"summer",
"fall"
];
export default function useCurrentSeason() { export default function useCurrentSeason() {
const { currentSeasonAnime } = useStaticQuery(graphql` const [ current, setCurrent ] = useState(null);
query {
currentSeasonAnime: allAnime { useEffect(() => {
groupedByYear: group(field: year) { if (!cache) {
year: fieldValue fetchCurrentSeason()
nodes { .then((current) => {
season cache = current;
}
} setCurrent(current);
} });
} else {
setCurrent(cache);
} }
`); }, []);
const currentYearGroup = currentSeasonAnime.groupedByYear.sort((a, b) => b.year - a.year)[0]; return {
const currentYear = currentYearGroup.year; currentYear: current?.year,
const currentSeason = seasonOrder[currentYearGroup.nodes.map(({ season }) => seasonOrder.indexOf(season.toLowerCase())).sort().pop()]; currentSeason: current?.season.toLowerCase()
};
return { currentYear, currentSeason };
} }
+1 -1
View File
@@ -1,4 +1,4 @@
import { fetchEntitySearchResults } from "lib/search"; import { fetchEntitySearchResults } from "lib/client/search";
import { useInfiniteQuery, useQueryClient } from "react-query"; import { useInfiniteQuery, useQueryClient } from "react-query";
export default function useEntitySearch(entity, query, params = {}) { export default function useEntitySearch(entity, query, params = {}) {
+2 -2
View File
@@ -5,10 +5,10 @@ export default function useImage(resourceWithImages) {
for (const image of resourceWithImages.images) { for (const image of resourceWithImages.images) {
switch (image.facet) { switch (image.facet) {
case "Small Cover": case "Small Cover":
images.smallCover = image.link; images.smallCover = image.link.replace("localhost", "staging.animethemes.moe");
break; break;
case "Large Cover": case "Large Cover":
images.largeCover = image.link; images.largeCover = image.link.replace("localhost", "staging.animethemes.moe");
break; break;
default: default:
// Ignore // Ignore
+9
View File
@@ -0,0 +1,9 @@
import { useEffect, useState } from "react";
export default function useLocation() {
const [ location, setLocation ] = useState({});
useEffect(() => setLocation(window.location), []);
return location;
}
+41
View File
@@ -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
};
}
-20
View File
@@ -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
}
}
}
`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

-76
View File
@@ -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>
);
}
+1
View File
@@ -0,0 +1 @@
export const baseUrl = process.env.NEXT_PUBLIC_API_URL;
+8
View File
@@ -0,0 +1,8 @@
import { baseUrl } from "lib/client/api";
export async function fetchCurrentSeason() {
const res = await fetch(`${baseUrl}/api/anime?sort=-year,-season&page[size]=1&fields[anime]=year,season`);
const json = await res.json();
return json.anime[0];
}
@@ -1,4 +1,4 @@
import { baseUrl } from "gatsby-source-animethemes/src"; import { baseUrl } from "lib/client/api";
const backLog = []; const backLog = [];
@@ -1,4 +1,4 @@
import { baseUrl } from "gatsby-source-animethemes/src"; import { baseUrl } from "lib/client/api";
const entityConfigs = { const entityConfigs = {
anime: { anime: {
+91
View File
@@ -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();
}
+41
View File
@@ -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
}
`;
+266
View File
@@ -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()
}
};

Some files were not shown because too many files have changed in this diff Show More