Merge branch 'main' of github.com:Accords-Library/accords-library.com

This commit is contained in:
DrMint 2022-03-28 18:04:55 +02:00
commit afbcbe7a3c
84 changed files with 8394 additions and 3008 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
*.js
*.ts

214
.eslintrc.js Normal file
View File

@ -0,0 +1,214 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: `./tsconfig.json`,
},
plugins: ["@typescript-eslint"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"next/core-web-vitals",
],
rules: {
/* POSSIBLES PROBLEMS */
// "array-callback-return": "error",
"no-await-in-loop": "error",
"no-constructor-return": "error",
"no-promise-executor-return": "error",
"no-self-compare": "error",
"no-template-curly-in-string": "error",
"no-unmodified-loop-condition": "error",
"no-unreachable-loop": "error",
"no-unused-private-class-members": "error",
// "no-use-before-define": "error",
"require-atomic-updates": "error",
/* SUGGESTIONS */
"accessor-pairs": "warn",
"arrow-body-style": "warn",
"block-scoped-var": "warn",
// camelcase: "warn",
// "capitalized-comments": "warn",
// "class-methods-use-this": "warn",
// complexity: "warn",
"consistent-return": "warn",
"consistent-this": "warn",
// curly: "warn",
"default-case": "warn",
"default-case-last": "warn",
eqeqeq: "error",
"func-name-matching": "warn",
"func-names": "warn",
"func-style": ["warn", "declaration"],
"grouped-accessor-pairs": "warn",
"guard-for-in": "warn",
"id-denylist": ["error", "data", "err", "e", "cb", "callback", "i"],
// "id-length": "warn",
"id-match": "warn",
"max-classes-per-file": ["error", 1],
// "max-depth": ["warn", 4],
// "max-lines": "warn",
// "max-lines-per-function": "warn",
// "max-nested-callbacks": "warn",
// "max-params": "warn",
// "max-statements": "warn",
"multiline-comment-style": "warn",
"new-cap": "warn",
"no-alert": "warn",
"no-bitwise": "warn",
"no-caller": "warn",
"no-confusing-arrow": "warn",
"no-continue": "warn",
"no-else-return": "warn",
"no-eq-null": "warn",
"no-eval": "warn",
"no-extend-native": "warn",
"no-extra-bind": "warn",
"no-floating-decimal": "warn",
"no-implicit-coercion": "warn",
"no-implicit-globals": "warn",
"no-inline-comments": "warn",
"no-iterator": "warn",
"no-label-var": "warn",
"no-labels": "warn",
"no-lone-blocks": "warn",
"no-lonely-if": "warn",
// "no-magic-numbers": "warn",
"no-mixed-operators": "warn",
"no-multi-assign": "warn",
"no-multi-str": "warn",
"no-negated-condition": "warn",
// "no-nested-ternary": "warn",
"no-new": "warn",
"no-new-func": "warn",
"no-new-object": "warn",
"no-new-wrappers": "warn",
"no-octal-escape": "warn",
"no-param-reassign": "warn",
"no-plusplus": "warn",
"no-proto": "warn",
"no-restricted-exports": "warn",
"no-restricted-globals": "warn",
"no-restricted-imports": "warn",
"no-restricted-properties": "warn",
"no-restricted-syntax": "warn",
"no-return-assign": "warn",
// "no-return-await": "warn",
"no-script-url": "warn",
"no-sequences": "warn",
// "no-ternary": "off",
"no-throw-literal": "warn",
"no-undef": "off",
"no-undef-init": "warn",
// "no-undefined": "warn",
// "no-underscore-dangle": "warn",
"no-unneeded-ternary": "warn",
"no-useless-call": "warn",
"no-useless-computed-key": "warn",
"no-useless-concat": "warn",
"no-useless-rename": "warn",
"no-useless-return": "warn",
"no-var": "warn",
"no-void": "warn",
"no-warning-comments": "warn",
// "object-shorthand": "warn",
"operator-assignment": "warn",
"prefer-arrow-callback": "warn",
"prefer-const": "warn",
"prefer-destructuring": ["warn", { array: false, object: true }],
"prefer-exponentiation-operator": "warn",
"prefer-named-capture-group": "warn",
"prefer-numeric-literals": "warn",
// "prefer-object-has-own": "warn",
"prefer-object-spread": "warn",
"prefer-promise-reject-errors": "warn",
"prefer-regex-literals": "warn",
"prefer-rest-params": "warn",
"prefer-spread": "warn",
"prefer-template": "warn",
// "quote-props": "warn",
radix: "warn",
"require-unicode-regexp": "warn",
// "sort-imports": "warn",
// "sort-keys": "warn",
"sort-vars": "warn",
"spaced-comment": "warn",
strict: "warn",
"symbol-description": "warn",
"vars-on-top": "warn",
yoda: "warn",
/* TYPESCRIPT */
"@typescript-eslint/array-type": "warn",
"@typescript-eslint/ban-tslint-comment": "warn",
"@typescript-eslint/class-literal-property-style": "warn",
"@typescript-eslint/consistent-indexed-object-style": "warn",
"@typescript-eslint/consistent-type-assertions": [
"warn",
{ assertionStyle: "as" },
],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/explicit-module-boundary-types": "warn",
"@typescript-eslint/method-signature-style": ["error", "property"],
"@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-confusing-non-null-assertion": "warn",
"@typescript-eslint/no-confusing-void-expression": [
"error",
{ ignoreArrowShorthand: true },
],
"@typescript-eslint/no-dynamic-delete": "error",
"@typescript-eslint/no-empty-interface": [
"error",
{ allowSingleExtends: true },
],
"@typescript-eslint/no-invalid-void-type": "error",
"@typescript-eslint/no-meaningless-void-operator": "error",
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
"@typescript-eslint/no-parameter-properties": "error",
"@typescript-eslint/no-require-imports": "error",
// "@typescript-eslint/no-type-alias": "warn",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
// "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "warn",
"@typescript-eslint/no-unnecessary-type-arguments": "warn",
"@typescript-eslint/prefer-enum-initializers": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-literal-enum-member": "error",
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/prefer-readonly": "warn",
// "@typescript-eslint/prefer-readonly-parameter-types": "warn",
"@typescript-eslint/prefer-reduce-type-parameter": "warn",
// "@typescript-eslint/prefer-regexp-exec": "warn",
"@typescript-eslint/prefer-return-this-type": "warn",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/sort-type-union-intersection-members": "warn",
// "@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/typedef": "error",
"@typescript-eslint/unified-signatures": "error",
/* EXTENSION OF ESLINT */
"@typescript-eslint/no-duplicate-imports": "error",
"@typescript-eslint/default-param-last": "warn",
"@typescript-eslint/dot-notation": "warn",
"@typescript-eslint/init-declarations": "warn",
"@typescript-eslint/no-array-constructor": "warn",
"@typescript-eslint/no-implied-eval": "warn",
"@typescript-eslint/no-invalid-this": "warn",
"@typescript-eslint/no-loop-func": "warn",
"@typescript-eslint/no-shadow": "warn",
"@typescript-eslint/no-unused-expressions": "warn",
"@typescript-eslint/no-useless-constructor": "warn",
"@typescript-eslint/require-await": "warn",
/* NEXTJS */
"@next/next/no-img-element": "off",
},
};

View File

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

37
.github/workflows/node.js.yml vendored Normal file
View File

@ -0,0 +1,37 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
# push:
# branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run build --if-present
env:
ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
NEXT_PUBLIC_URL_CMS: ${{ secrets.NEXT_PUBLIC_URL_CMS }}
NEXT_PUBLIC_URL_IMG: ${{ secrets.NEXT_PUBLIC_URL_IMG }}
NEXT_PUBLIC_URL_SELF: ${{ secrets.NEXT_PUBLIC_URL_SELF }}
URL_GRAPHQL: ${{ secrets.URL_GRAPHQL }}

1
.gitignore vendored
View File

@ -16,6 +16,7 @@
# production
/build
/public/sitemap*
# misc
.DS_Store

10
.hintrc Normal file
View File

@ -0,0 +1,10 @@
{
"extends": ["development"],
"hints": {
"no-inline-styles": "off",
"apple-touch-icons": "off",
"compat-api/html": "off",
"axe/text-alternatives": "off",
"axe/parsing": "off"
}
}

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
.next

View File

@ -1,5 +1,9 @@
# Accords-library.com
[![Node.js CI](https://github.com/Accords-Library/accords-library.com/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/Accords-Library/accords-library.com/actions/workflows/node.js.yml)
[![GitHub](https://img.shields.io/github/license/Accords-Library/accords-library.com?style=flat-square)](https://github.com/Accords-Library/accords-library.com/blob/main/LICENSE)
![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/Accords-Library/accords-library.com?style=flat-square)
## Technologies
#### [Back](https://github.com/Accords-Library/strapi.accords-library.com)
@ -9,6 +13,14 @@
- Multilanguage support
- Markdown format for the rich text fields
#### [Image Processor](https://github.com/Accords-Library/img.accords-library.com)
- Convert the images from the CMS to 4 formats
- Small: 512x512, quality 60, .webp
- Medium: 1024x1024, quality 75, .webp
- Large: 2048x2048, quality 80, .webp
- Og: 512x512, quality 60, .jpg
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
- Language: [TypeScript](https://www.typescriptlang.org/)
@ -29,6 +41,12 @@
- State Management: [React Context](https://reactjs.org/docs/context.html)
- Persistent app state using LocalStorage
- Support for many screen sizes and resolutions
- SSG (Static Site Generation):
- The website is built before running in production
- Performances are great, and possibility to deploy the app using a CDN
- OpenGraph and Metadata
- Good defaults for the metadate and OpenGraph properties
- Each page can provide the thumbnail, title, description to be used
- Data quality testing
- Data from the CMS is subject to a battery of tests (about 20 warning types and 40 error types) at build time
- Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it.
@ -53,6 +71,9 @@ Enter the followind information:
```txt
URL_GRAPHQL=https://url-to.strapi-accords-library.com/graphql
ACCESS_TOKEN=genatedcode-by-strapi-api
SMTP_HOST=email.provider.com
SMTP_USER=email@example.com
SMTP_PASSWORD=mypassword123
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com/
NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com/
NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com

10
next-env.d.ts vendored
View File

@ -1,5 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

28
next-sitemap.js Normal file
View File

@ -0,0 +1,28 @@
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.NEXT_PUBLIC_URL_SELF && "https://accords-library.com",
generateRobotsTxt: true,
alternateRefs: [
{
href: `${process.env.NEXT_PUBLIC_URL_SELF}/en/`,
hreflang: "en",
},
{
href: `${process.env.NEXT_PUBLIC_URL_SELF}/fr/`,
hreflang: "fr",
},
{
href: `${process.env.NEXT_PUBLIC_URL_SELF}/ja/`,
hreflang: "ja",
},
{
href: `${process.env.NEXT_PUBLIC_URL_SELF}/es/`,
hreflang: "es",
},
{
href: `${process.env.NEXT_PUBLIC_URL_SELF}/pt-br/`,
hreflang: "pt-br",
},
],
exclude: ["/en/*", "/fr/*", "/ja/*", "/es/*", "/pt-br/*"],
};

View File

@ -3,7 +3,7 @@ module.exports = {
swcMinify: true,
reactStrictMode: true,
i18n: {
locales: ["en", "fr", "ja", "es", "xx"],
locales: ["en", "fr", "ja", "es", "pt-br"],
defaultLocale: "en",
},
images: {

744
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,30 +4,39 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"postbuild": "next-sitemap",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fontsource/material-icons": "^4.5.2",
"@fontsource/material-icons-rounded": "^4.5.2",
"@fontsource/opendyslexic": "^4.5.2",
"@fontsource/vollkorn": "^4.5.4",
"@fontsource/zen-maru-gothic": "^4.5.5",
"markdown-to-jsx": "^7.1.6",
"@tippyjs/react": "^4.2.6",
"markdown-to-jsx": "^7.1.7",
"next": "^12.1.0",
"nodemailer": "^6.7.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-image-lightbox": "^5.1.4",
"react-swipeable": "^6.2.0",
"react-tooltip": "^4.2.21",
"turndown": "^7.1.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.2",
"@types/node": "17.0.18",
"@types/react": "17.0.39",
"@types/react-dom": "^17.0.11",
"eslint": "8.9.0",
"@types/node": "17.0.21",
"@types/nodemailer": "^6.4.4",
"@types/react": "17.0.40",
"@types/react-dom": "^17.0.13",
"@types/turndown": "^5.0.1",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"eslint": "^8.10.0",
"eslint-config-next": "12.1.0",
"next-sitemap": "^2.5.14",
"prettier-plugin-organize-imports": "^2.3.4",
"tailwindcss": "^3.0.23",
"typescript": "4.5.5"
"typescript": "^4.6.2"
}
}

View File

@ -2,5 +2,5 @@ module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
},
};

View File

@ -21,4 +21,4 @@ Insert the following code in the `head` section of your pages:
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="theme-color" content="#feecd6">
*Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker)
_Optional_ - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker)

View File

@ -1,10 +1,10 @@
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#9c6644">
<meta name="apple-mobile-web-app-title" content="Accord's Library">
<meta name="application-name" content="Accord's Library">
<meta name="msapplication-TileColor" content="#feecd6">
<meta name="msapplication-TileImage" content="/mstile-144x144.png">
<meta name="theme-color" content="#feecd6">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#9c6644" />
<meta name="apple-mobile-web-app-title" content="Accord's Library" />
<meta name="application-name" content="Accord's Library" />
<meta name="msapplication-TileColor" content="#feecd6" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
<meta name="theme-color" content="#feecd6" />

9
public/robots.txt Normal file
View File

@ -0,0 +1,9 @@
# *
User-agent: *
Allow: /
# Host
Host: https://accords-library.com
# Sitemaps
Sitemap: https://accords-library.com/sitemap.xml

1
run_accords_prettier.sh Executable file
View File

@ -0,0 +1 @@
npx prettier --end-of-line auto --write .

View File

@ -1,4 +1,2 @@
NODE_ENV=test
# npx next build | tee ./testing_logs/$(date +"%Y-%m-%d---%H-%M-%S").log
npx next build 2> >(tee ./testing_logs/$(date +"%Y-%m-%d---%H-%M-%S").stderr.tsv) 1> >(tee ./testing_logs/$(date +"%Y-%m-%d---%H-%M-%S").stdout.tsv)
export ENABLE_TESTING_LOG=true
npx next build 2> >(tee ./testing_logs/$(date +"%Y-%m-%d---%H-%M-%S").stderr.tsv) 1> >(tee ./testing_logs/$(date +"%Y-%m-%d---%H-%M-%S").stdout.tsv)

View File

@ -1,34 +1,32 @@
import {
GetWebsiteInterfaceQuery,
StrapiImage,
} from "graphql/operations-types";
import MainPanel from "./Panels/MainPanel";
import Head from "next/head";
import { useSwipeable } from "react-swipeable";
import { useRouter } from "next/router";
import Button from "components/Button";
import { getOgImage, OgImage, prettyLanguage } from "queries/helpers";
import { useMediaCoarse, useMediaMobile } from "hooks/useMediaQuery";
import ReactTooltip from "react-tooltip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { StrapiImage } from "graphql/operations-types";
import { useMediaMobile } from "hooks/useMediaQuery";
import Head from "next/head";
import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getOgImage, OgImage, prettyLanguage } from "queries/helpers";
import { useEffect, useState } from "react";
import { useSwipeable } from "react-swipeable";
import { ImageQuality } from "./Img";
import MainPanel from "./Panels/MainPanel";
import Popup from "./Popup";
import Select from "./Select";
type AppLayoutProps = {
interface AppLayoutProps extends AppStaticProps {
subPanel?: React.ReactNode;
subPanelIcon?: string;
contentPanel?: React.ReactNode;
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
title?: string;
navTitle: string;
thumbnail?: StrapiImage;
description?: string;
extra?: React.ReactNode;
};
}
export default function AppLayout(props: AppLayoutProps): JSX.Element {
const { langui, currencies, languages, subPanel, contentPanel } = props;
const router = useRouter();
const isMobile = useMediaMobile();
const isCoarse = useMediaCoarse();
const appLayout = useAppLayout();
const sensibilitySwipe = 1.1;
@ -38,7 +36,7 @@ export default function AppLayout(props: AppLayoutProps): JSX.Element {
if (SwipeEventData.velocity < sensibilitySwipe) return;
if (appLayout.mainPanelOpen) {
appLayout.setMainPanelOpen(false);
} else if (props.subPanel && props.contentPanel) {
} else if (subPanel && contentPanel) {
appLayout.setSubPanelOpen(true);
}
},
@ -52,28 +50,7 @@ export default function AppLayout(props: AppLayoutProps): JSX.Element {
},
});
const mainPanelClass = `fixed desktop:left-0 desktop:top-0 desktop:bottom-0 ${
appLayout.mainPanelReduced ? "desktop:w-[6rem]" : "desktop:w-[20rem]"
}`;
const subPanelClass = `fixed desktop:top-0 desktop:bottom-0 desktop:w-[20rem] ${
appLayout.mainPanelReduced ? " desktop:left-[6rem]" : "desktop:left-[20rem]"
}`;
let contentPanelClass = "";
if (props.subPanel) {
contentPanelClass = `fixed desktop:top-0 desktop:bottom-0 desktop:right-0 ${
appLayout.mainPanelReduced
? "desktop:left-[26rem]"
: "desktop:left-[40rem]"
}`;
} else if (props.contentPanel) {
contentPanelClass = `fixed desktop:top-0 desktop:bottom-0 desktop:right-0 ${
appLayout.mainPanelReduced
? "desktop:left-[6rem]"
: "desktop:left-[20rem]"
}`;
}
const turnSubIntoContent = props.subPanel && !props.contentPanel;
const turnSubIntoContent = subPanel && !contentPanel;
const titlePrefix = "Accords Library";
const metaImage: OgImage = props.thumbnail
@ -88,13 +65,58 @@ export default function AppLayout(props: AppLayoutProps): JSX.Element {
const metaDescription = props.description
? props.description
: "Accord's Library aims at gathering and archiving all of Yoko Taros work. Yoko Taro is a Japanese video game director and scenario writer.";
: langui.default_description;
useEffect(() => {
document.getElementsByTagName("html")[0].style.fontSize = `${
(appLayout.fontSize ?? 1) * 100
}%`;
}, [appLayout.fontSize]);
const currencyOptions = currencies.map(
(currency) => currency.attributes.code
);
const [currencySelect, setCurrencySelect] = useState<number>(-1);
useEffect(() => {
if (appLayout.currency)
setCurrencySelect(currencyOptions.indexOf(appLayout.currency));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.currency]);
useEffect(() => {
if (currencySelect >= 0)
appLayout.setCurrency(currencyOptions[currencySelect]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currencySelect]);
let gridCol = "";
if (props.subPanel) {
if (appLayout.mainPanelReduced) {
gridCol = "grid-cols-[6rem_20rem_1fr]";
} else {
gridCol = "grid-cols-[20rem_20rem_1fr]";
}
} else if (appLayout.mainPanelReduced) {
gridCol = "grid-cols-[6rem_0px_1fr]";
} else {
gridCol = "grid-cols-[20rem_0px_1fr]";
}
return (
<div className={appLayout.darkMode ? "set-theme-dark" : "set-theme-light"}>
<div
id="MyAppLayout"
className={`${
appLayout.darkMode ? "set-theme-dark" : "set-theme-light"
} ${
appLayout.dyslexic
? "set-theme-font-dyslexic"
: "set-theme-font-standard"
}`}
>
<div
{...handlers}
className="fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black"
className={`fixed inset-0 touch-pan-y p-0 m-0 bg-light text-black grid [grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr] mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`}
>
<Head>
<title>{`${titlePrefix} - ${ogTitle}`}</title>
@ -104,12 +126,8 @@ export default function AppLayout(props: AppLayoutProps): JSX.Element {
content={`${titlePrefix} - ${ogTitle}`}
></meta>
{props.description && (
<>
<meta name="description" content={metaDescription} />
<meta name="twitter:description" content={metaDescription}></meta>
</>
)}
<meta name="description" content={metaDescription} />
<meta name="twitter:description" content={metaDescription}></meta>
<meta property="og:image" content={metaImage.image}></meta>
<meta property="og:image:secure_url" content={metaImage.image}></meta>
@ -128,145 +146,247 @@ export default function AppLayout(props: AppLayoutProps): JSX.Element {
<meta name="twitter:image" content={metaImage.image}></meta>
</Head>
{/* Background when navbar is opened */}
<div
className={`[grid-area:content] mobile:z-10 absolute inset-0 transition-[backdrop-filter] duration-500 ${
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
? "[backdrop-filter:blur(2px)]"
: "pointer-events-none touch-none "
}`}
>
<div
className={`absolute bg-shade inset-0 transition-opacity duration-500
${turnSubIntoContent ? "" : ""}
${
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
? "opacity-60"
: "opacity-0"
}`}
onClick={() => {
appLayout.setMainPanelOpen(false);
appLayout.setSubPanelOpen(false);
}}
></div>
</div>
{/* Content panel */}
<div
className={`[grid-area:content] overflow-y-scroll bg-light texture-paper-dots`}
>
{contentPanel ? (
contentPanel
) : (
<div className="grid place-content-center h-full">
<div className="text-dark border-dark border-2 border-dotted rounded-2xl p-8 grid grid-flow-col place-items-center gap-9 opacity-40">
<p className="text-4xl"></p>
<p className="text-2xl w-64">{langui.select_option_sidebar}</p>
</div>
</div>
)}
</div>
{/* Sub panel */}
{subPanel && (
<div
className={`[grid-area:sub] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-end border-r-[1px] mobile:border-r-0 mobile:border-l-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
${
turnSubIntoContent
? "mobile:border-l-0 mobile:w-full"
: !appLayout.subPanelOpen && "mobile:translate-x-[100vw]"
}`}
>
{subPanel}
</div>
)}
{/* Main panel */}
<div
className={`[grid-area:main] mobile:[grid-area:content] mobile:z-10 mobile:w-[90%] mobile:justify-self-start border-r-[1px] border-black border-dotted overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
${appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full"}`}
>
<MainPanel langui={langui} />
</div>
{/* Navbar */}
<div className="fixed inset-0 top-auto h-20 border-t-[1px] border-black border-dotted grid grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots">
<div className="[grid-area:navbar] border-t-[1px] border-black border-dotted grid grid-cols-[5rem_1fr_5rem] place-items-center desktop:hidden bg-light texture-paper-dots">
<span
className="material-icons mt-[.1em] cursor-pointer"
onClick={() => appLayout.setMainPanelOpen(true)}
onClick={() => {
appLayout.setMainPanelOpen(!appLayout.mainPanelOpen);
appLayout.setSubPanelOpen(false);
}}
>
menu
{appLayout.mainPanelOpen ? "close" : "menu"}
</span>
<p className="text-2xl font-black font-headers">{props.navTitle}</p>
<span
className="material-icons mt-[.1em] cursor-pointer"
onClick={() => appLayout.setSubPanelOpen(true)}
onClick={() => {
appLayout.setSubPanelOpen(!appLayout.subPanelOpen);
appLayout.setMainPanelOpen(false);
}}
>
{props.subPanel && !turnSubIntoContent
? props.subPanelIcon
{subPanel && !turnSubIntoContent
? appLayout.subPanelOpen
? "close"
: props.subPanelIcon
? props.subPanelIcon
: "tune"
: ""}
</span>
</div>
{/* Content panel */}
<div
className={`top-0 left-0 right-0 bottom-20 overflow-y-scroll bg-light texture-paper-dots ${contentPanelClass}`}
<Popup
state={appLayout.languagePanelOpen}
setState={appLayout.setLanguagePanelOpen}
>
{props.contentPanel ? (
props.contentPanel
) : (
<div className="grid place-content-center h-full">
<div className="text-dark border-dark border-2 border-dotted rounded-2xl p-8 grid grid-flow-col place-items-center gap-9 opacity-40">
<p className="text-4xl"></p>
<p className="text-2xl w-64">
Select one of the options in the sidebar
</p>
<h2 className="text-2xl">{langui.select_language}</h2>
<div className="flex flex-wrap flex-row gap-2 mobile:flex-col">
{router.locales?.map((locale) => (
<Button
key={locale}
active={locale === router.locale}
href={router.asPath}
locale={locale}
onClick={() => appLayout.setLanguagePanelOpen(false)}
>
{prettyLanguage(locale, languages)}
</Button>
))}
</div>
</Popup>
<Popup
state={appLayout.configPanelOpen}
setState={appLayout.setConfigPanelOpen}
>
<h2 className="text-2xl">{langui.settings}</h2>
<div className="mt-4 grid gap-8 place-items-center text-center desktop:grid-cols-2">
<div>
<h3 className="text-xl">{langui.theme}</h3>
<div className="flex flex-row">
<Button
onClick={() => {
appLayout.setDarkMode(false);
appLayout.setSelectedThemeMode(true);
}}
active={
appLayout.selectedThemeMode === true &&
appLayout.darkMode === false
}
className="rounded-r-none"
>
{langui.light}
</Button>
<Button
onClick={() => {
appLayout.setSelectedThemeMode(false);
}}
active={appLayout.selectedThemeMode === false}
className="rounded-l-none rounded-r-none border-x-0"
>
{langui.auto}
</Button>
<Button
onClick={() => {
appLayout.setDarkMode(true);
appLayout.setSelectedThemeMode(true);
}}
active={
appLayout.selectedThemeMode === true &&
appLayout.darkMode === true
}
className="rounded-l-none"
>
{langui.dark}
</Button>
</div>
</div>
)}
</div>
{/* Background when navbar is opened */}
<div
className={`fixed bg-shade inset-0 transition-opacity duration-500
${turnSubIntoContent ? "z-10" : ""}
${
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
? "opacity-60"
: "opacity-0 pointer-events-none touch-none"
}`}
onClick={() => {
appLayout.setMainPanelOpen(false);
appLayout.setSubPanelOpen(false);
}}
></div>
<div>
<h3 className="text-xl">{langui.currency}</h3>
<div>
<Select
options={currencyOptions}
state={currencySelect}
setState={setCurrencySelect}
className="w-28"
/>
</div>
</div>
{/* Sub panel */}
{props.subPanel ? (
<div
className={`${subPanelClass} border-r-[1px] mobile:border-r-0 mobile:border-l-[1px] border-black border-dotted top-0 bottom-0 right-0 left-12 overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 bg-light texture-paper-dots
${
turnSubIntoContent
? "mobile:translate-x-0 mobile:bottom-20 mobile:left-0 mobile:border-l-0"
: !appLayout.subPanelOpen
? "mobile:translate-x-full"
: ""
}`}
>
{props.subPanel}
</div>
) : (
""
)}
{/* Main panel */}
<div
className={`${mainPanelClass} border-r-[1px] border-black border-dotted top-0 bottom-0 left-0 right-12 overflow-y-scroll webkit-scrollbar:w-0 [scrollbar-width:none] transition-transform duration-300 z-20 bg-light texture-paper-dots
${appLayout.mainPanelOpen ? "" : "mobile:-translate-x-full"}`}
>
<MainPanel langui={props.langui} />
</div>
{/* Main panel minimize button*/}
<div
className={`mobile:hidden translate-x-0 fixed top-1/2 z-20 ${
appLayout.mainPanelReduced ? "left-[4.65rem]" : "left-[18.65rem]"
}`}
onClick={() =>
appLayout.setMainPanelReduced(!appLayout.mainPanelReduced)
}
>
<Button className="material-icons bg-light !px-2">
{appLayout.mainPanelReduced ? "chevron_right" : "chevron_left"}
</Button>
</div>
{/* Language selection background */}
<div
className={`fixed bg-shade inset-0 transition-all duration-500 z-20 grid place-content-center ${
appLayout.languagePanelOpen
? "bg-opacity-60"
: "bg-opacity-0 pointer-events-none touch-none"
}`}
onClick={() => {
appLayout.setLanguagePanelOpen(false);
}}
>
<div
className={`p-10 bg-light rounded-lg shadow-2xl shadow-shade grid gap-4 place-items-center transition-transform ${
appLayout.languagePanelOpen ? "scale-100" : "scale-0"
}`}
>
<h2 className="text-2xl">Select a language</h2>
<div className="flex flex-wrap flex-row gap-2">
{router.locales?.sort().map((locale) => (
<div>
<h3 className="text-xl">{langui.font_size}</h3>
<div className="flex flex-row">
<Button
key={locale}
active={locale === router.locale}
href={router.asPath}
locale={locale}
className="rounded-r-none"
onClick={() =>
appLayout.setFontSize(
appLayout.fontSize ? appLayout.fontSize / 1.05 : 1 / 1.05
)
}
>
{prettyLanguage(locale)}
<span className="material-icons">text_decrease</span>
</Button>
))}
<Button
className="rounded-l-none rounded-r-none border-x-0"
onClick={() => appLayout.setFontSize(1)}
>
{((appLayout.fontSize ?? 1) * 100).toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
%
</Button>
<Button
className="rounded-l-none"
onClick={() =>
appLayout.setFontSize(
appLayout.fontSize ? appLayout.fontSize * 1.05 : 1 * 1.05
)
}
>
<span className="material-icons">text_increase</span>
</Button>
</div>
</div>
<div>
<h3 className="text-xl">{langui.font}</h3>
<div className="grid gap-2">
<Button
active={appLayout.dyslexic === false}
onClick={() => appLayout.setDyslexic(false)}
className="font-zenMaruGothic"
>
Zen Maru Gothic
</Button>
<Button
active={appLayout.dyslexic === true}
onClick={() => appLayout.setDyslexic(true)}
className="font-openDyslexic"
>
OpenDyslexic
</Button>
</div>
</div>
<div>
<h3 className="text-xl">{langui.player_name}</h3>
<input
type="text"
placeholder="<player>"
className="w-48"
onInput={(event) =>
appLayout.setPlayerName(
(event.target as HTMLInputElement).value
)
}
/>
</div>
</div>
</div>
<ReactTooltip
id="MainPanelTooltip"
place="right"
type="light"
effect="solid"
delayShow={300}
delayHide={100}
disable={!appLayout.mainPanelReduced || isMobile || isCoarse}
className="drop-shadow-shade-xl !opacity-100 !bg-light !rounded-lg after:!border-r-light text-left !text-black"
/>
</Popup>
</div>
{props.extra}
</div>
);
}

View File

@ -1,17 +1,19 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { MouseEventHandler } from "react";
type ButtonProps = {
id?: string;
className?: string;
href?: string;
children: React.ReactChild | React.ReactChild[];
children: React.ReactNode;
active?: boolean;
locale?: string;
onClick?: MouseEventHandler<HTMLDivElement>;
};
export default function Button(props: ButtonProps): JSX.Element {
const router = useRouter();
const button = (
<div
id={props.id}
@ -21,19 +23,23 @@ export default function Button(props: ButtonProps): JSX.Element {
} ${
props.active
? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed"
: "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:drop-shadow-black-lg active:border-black"
: "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black"
}`}
>
{props.children}
</div>
);
const result = props.href ? (
<Link href={props.href} locale={props.locale} passHref>
return (
<div
onClick={() => {
if (props.href || props.locale)
router.push(props.href ?? router.asPath, props.href, {
locale: props.locale,
});
}}
>
{button}
</Link>
) : (
button
</div>
);
return result;
}

View File

@ -1,25 +1,12 @@
type ChipProps = {
className?: string;
children: React.ReactChild | React.ReactChild[];
"data-tip"?: string;
"data-for"?: string;
"data-html"?: boolean;
"data-multiline"?: boolean;
children: React.ReactNode;
};
export default function Chip(props: ChipProps): JSX.Element {
return (
<div
className={`grid place-content-center place-items-center text-xs pb-[0.14rem] px-1.5 border-[1px] rounded-full opacity-70 transition-[color,_opacity,border-color] ${
props.className
} ${
props["data-tip"] &&
"hover:text-dark hover:border-dark hover:opacity-100"
}`}
data-tip={props["data-tip"]}
data-for={props["data-for"]}
data-html={props["data-html"]}
data-multiline={props["data-multiline"]}
className={`grid place-content-center place-items-center text-xs pb-[0.14rem] whitespace-nowrap px-1.5 border-[1px] rounded-full opacity-70 transition-[color,_opacity,_border-color] hover:opacity-100 ${props.className}`}
>
{props.children}
</div>

View File

@ -1,35 +1,40 @@
import Chip from "components/Chip";
import ToolTip from "components/ToolTip";
import {
Enum_Componenttranslationschronologyitem_Status,
GetChronologyItemsQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { getStatusDescription } from "queries/helpers";
export type ChronologyItemComponentProps = {
item: GetChronologyItemsQuery["chronologyItems"]["data"][number];
displayYear: boolean;
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function ChronologyItemComponent(
props: ChronologyItemComponentProps
): JSX.Element {
const { langui } = props;
function generateAnchor(year: number, month: number, day: number): string {
let result: string = "";
let result = "";
result += year;
if (month) result += "-" + month.toString().padStart(2, "0");
if (day) result += "-" + day.toString().padStart(2, "0");
if (month) result += `- ${month.toString().padStart(2, "0")}`;
if (day) result += `- ${day.toString().padStart(2, "0")}`;
return result;
}
function generateYear(displayed_date: string, year: number): string {
if (displayed_date) {
return displayed_date;
} else {
return year.toString();
}
return year.toString();
}
function generateDate(month: number, day: number): string {
let lut = [
const lut = [
"Jan",
"Feb",
"Mar",
@ -44,11 +49,11 @@ export default function ChronologyItemComponent(
"Dec",
];
let result: string = "";
let result = "";
if (month) {
result += lut[month - 1];
if (day) {
result += " " + day;
result += ` ${day}`;
}
}
@ -64,15 +69,13 @@ export default function ChronologyItemComponent(
props.item.attributes.day
)}
>
{props.displayYear ? (
{props.displayYear && (
<p className="text-lg mt-[-.2em] font-bold">
{generateYear(
props.item.attributes.displayed_date,
props.item.attributes.year
)}
</p>
) : (
""
)}
<p className="col-start-1 text-dark text-sm">
@ -87,28 +90,17 @@ export default function ChronologyItemComponent(
<div className="place-items-start place-content-start grid grid-flow-col gap-2">
{translation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && (
<Chip
data-tip={
translation.status ===
Enum_Componenttranslationschronologyitem_Status.Incomplete
? "This entry is only partially translated/transcribed."
: translation.status ===
Enum_Componenttranslationschronologyitem_Status.Draft
? "This entry is just a draft. It usually means that this is a work-in-progress. Translation/transcription might be poor and/or computer-generated."
: translation.status ===
Enum_Componenttranslationschronologyitem_Status.Review
? "This entry has not yet being proofread. The content should still be accurate."
: ""
}
data-for={"ChronologyTooltip"}
<ToolTip
content={getStatusDescription(translation.status, langui)}
maxWidth={"20rem"}
>
{translation.status}
</Chip>
<Chip>{translation.status}</Chip>
</ToolTip>
)}
{translation.title ? <h3>{translation.title}</h3> : ""}
</div>
{translation.description ? (
{translation.description && (
<p
className={
event.translations.length > 1
@ -118,11 +110,9 @@ export default function ChronologyItemComponent(
>
{translation.description}
</p>
) : (
""
)}
{translation.note ? (
<em>{"Notes: " + translation.note}</em>
<em>{`Notes: ${translation.note}`}</em>
) : (
""
)}
@ -131,7 +121,7 @@ export default function ChronologyItemComponent(
<p className="text-dark text-xs grid place-self-start grid-flow-col gap-1 mt-1">
{event.source.data ? (
"(" + event.source.data.attributes.name + ")"
`(${event.source.data.attributes.name})`
) : (
<>
<span className="material-icons !text-sm">warning</span>No

View File

@ -1,14 +1,20 @@
import ChronologyItemComponent from "components/Chronology/ChronologyItemComponent";
import { GetChronologyItemsQuery } from "graphql/operations-types";
import {
GetChronologyItemsQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
type ChronologyYearComponentProps = {
year: number;
items: GetChronologyItemsQuery["chronologyItems"]["data"][number][];
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function ChronologyYearComponent(
props: ChronologyYearComponentProps
): JSX.Element {
const { langui } = props;
return (
<div
className="target:bg-mid rounded-2xl target:py-4 target:my-4"
@ -19,6 +25,7 @@ export default function ChronologyYearComponent(
key={index}
item={item}
displayYear={index === 0}
langui={langui}
/>
))}
</div>

View File

@ -1,85 +1,90 @@
import Chip from "components/Chip";
import Img, { ImageQuality } from "components/Img";
import InsetBox from "components/InsetBox";
import {
GetContentQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { prettySlug } from "queries/helpers";
import Button from "components/Button";
import Img, { ImageQuality } from "components/Img";
import InsetBox from "components/InsetBox";
import { prettyinlineTitle, prettySlug, slugify } from "queries/helpers";
export type ThumbnailHeaderProps = {
content: {
slug: GetContentQuery["contents"]["data"][number]["attributes"]["slug"];
thumbnail: GetContentQuery["contents"]["data"][number]["attributes"]["thumbnail"];
titles: GetContentQuery["contents"]["data"][number]["attributes"]["titles"];
type: GetContentQuery["contents"]["data"][number]["attributes"]["type"];
categories: GetContentQuery["contents"]["data"][number]["attributes"]["categories"];
};
pre_title?: string;
title: string;
subtitle?: string;
description?: string;
type?: GetContentQuery["contents"]["data"][number]["attributes"]["type"];
categories?: GetContentQuery["contents"]["data"][number]["attributes"]["categories"];
thumbnail?: GetContentQuery["contents"]["data"][number]["attributes"]["thumbnail"]["data"]["attributes"];
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function ThumbnailHeader(
props: ThumbnailHeaderProps
): JSX.Element {
const content = props.content;
const langui = props.langui;
const {
langui,
pre_title,
title,
subtitle,
thumbnail,
type,
categories,
description,
} = props;
return (
<>
<div className="grid place-items-center gap-12 mb-12">
<div className="drop-shadow-shade-lg">
{content.thumbnail.data ? (
{thumbnail ? (
<Img
className=" rounded-xl"
image={content.thumbnail.data.attributes}
image={thumbnail}
quality={ImageQuality.Medium}
priority
/>
) : (
<div className="w-full aspect-[4/3] bg-light rounded-xl"></div>
<div className="w-96 aspect-[4/3] bg-light rounded-xl"></div>
)}
</div>
<div className="grid place-items-center text-center">
{content.titles.length > 0 ? (
<>
<p className="text-2xl">{content.titles[0].pre_title}</p>
<h1 className="text-3xl">{content.titles[0].title}</h1>
<h2 className="text-2xl">{content.titles[0].subtitle}</h2>
</>
) : (
<h1 className="text-3xl">{prettySlug(content.slug)}</h1>
<div
id={slugify(
prettyinlineTitle(pre_title ?? "", title, subtitle ?? "")
)}
className="grid place-items-center text-center"
>
<p className="text-2xl">{pre_title}</p>
<h1 className="text-3xl">{title}</h1>
<h2 className="text-2xl">{subtitle}</h2>
</div>
</div>
<div className="grid grid-flow-col gap-8">
{content.type ? (
<div className="grid place-items-center place-content-start gap-2">
<h3 className="text-xl">{langui.global_type}</h3>
<Button>
{content.type.data.attributes.titles.length > 0
? content.type.data.attributes.titles[0].title
: prettySlug(content.type.data.attributes.slug)}
</Button>
{type?.data && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.type}</h3>
<div className="flex flex-row flex-wrap">
<Chip>
{type.data.attributes.titles.length > 0
? type.data.attributes.titles[0].title
: prettySlug(type.data.attributes.slug)}
</Chip>
</div>
</div>
) : (
""
)}
{content.categories.data.length > 0 ? (
<div className="grid place-items-center place-content-start gap-2">
<h3 className="text-xl">{langui.global_categories}</h3>
{content.categories.data.map((category) => (
<Button key={category.id}>{category.attributes.name}</Button>
))}
{categories && categories.data.length > 0 && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{categories.data.map((category) => (
<Chip key={category.id}>{category.attributes.name}</Chip>
))}
</div>
</div>
) : (
""
)}
</div>
{content.titles.length > 0 && content.titles[0].description && (
<InsetBox className="mt-8">{content.titles[0].description}</InsetBox>
)}
{description && <InsetBox className="mt-8">{description}</InsetBox>}
</>
);
}

View File

@ -1,6 +1,5 @@
import { StrapiImage } from "graphql/operations-types";
import { ImageProps } from "next/image";
import Image from "next/image";
import Image, { ImageProps } from "next/image";
export enum ImageQuality {
Small = "small",
@ -10,11 +9,12 @@ export enum ImageQuality {
}
export function getAssetURL(url: string, quality: ImageQuality): string {
url = url.replace(/^\/uploads/, "/" + quality);
url = url.replace(/.jpg$/, ".webp");
url = url.replace(/.png$/, ".webp");
if (quality === ImageQuality.Og) url = url.replace(/.webp$/, ".jpg");
return process.env.NEXT_PUBLIC_URL_IMG + url;
let newUrl = url;
newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`);
newUrl = newUrl.replace(/.jpg$/u, ".webp");
newUrl = newUrl.replace(/.png$/u, ".webp");
if (quality === ImageQuality.Og) newUrl = newUrl.replace(/.webp$/u, ".jpg");
return process.env.NEXT_PUBLIC_URL_IMG + newUrl;
}
export function getImgSizesByMaxSize(
@ -25,10 +25,9 @@ export function getImgSizesByMaxSize(
if (width > height) {
if (width < maxSize) return { width: width, height: height };
return { width: maxSize, height: (height / width) * maxSize };
} else {
if (height < maxSize) return { width: width, height: height };
return { width: (width / height) * maxSize, height: maxSize };
}
if (height < maxSize) return { width: width, height: height };
return { width: (width / height) * maxSize, height: maxSize };
}
export function getImgSizesByQuality(
@ -45,12 +44,14 @@ export function getImgSizesByQuality(
return getImgSizesByMaxSize(width, height, 1024);
case ImageQuality.Large:
return getImgSizesByMaxSize(width, height, 2048);
default:
return { width: 0, height: 0 };
}
}
type ImgProps = {
className?: string;
image: StrapiImage;
image?: StrapiImage;
quality?: ImageQuality;
alt?: ImageProps["alt"];
layout?: ImageProps["layout"];
@ -60,27 +61,28 @@ type ImgProps = {
};
export default function Img(props: ImgProps): JSX.Element {
const imgSize = getImgSizesByQuality(
props.image.width,
props.image.height,
props.quality ? props.quality : ImageQuality.Small
);
if (props.rawImg) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
className={props.className}
src={getAssetURL(
props.image.url,
props.quality ? props.quality : ImageQuality.Small
)}
alt={props.alt ? props.alt : props.image.alternativeText}
width={imgSize.width}
height={imgSize.height}
/>
if (props.image) {
const imgSize = getImgSizesByQuality(
props.image.width,
props.image.height,
props.quality ? props.quality : ImageQuality.Small
);
} else {
if (props.rawImg) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
className={props.className}
src={getAssetURL(
props.image.url,
props.quality ? props.quality : ImageQuality.Small
)}
alt={props.alt ? props.alt : props.image.alternativeText}
width={imgSize.width}
height={imgSize.height}
/>
);
}
return (
<Image
className={props.className}
@ -98,4 +100,5 @@ export default function Img(props: ImgProps): JSX.Element {
/>
);
}
return <></>;
}

View File

@ -1,6 +1,6 @@
type InsetBoxProps = {
className?: string;
children: React.ReactChild | React.ReactChild[];
children: React.ReactNode;
id?: string;
};

View File

@ -0,0 +1,42 @@
import {
GetLanguagesQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { useRouter } from "next/router";
import { prettyLanguage } from "queries/helpers";
import Button from "./Button";
type HorizontalLineProps = {
className?: string;
locales: string[];
languages: GetLanguagesQuery["languages"]["data"];
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
href?: string;
};
export default function HorizontalLine(
props: HorizontalLineProps
): JSX.Element {
const { locales, langui, href } = props;
const router = useRouter();
return (
<div className="w-full grid place-content-center">
<div className="flex flex-col place-items-center text-center gap-4 my-12 border-2 border-mid rounded-xl p-8 max-w-lg">
<p>{langui.language_switch_message}</p>
<div className="flex flex-wrap flex-row gap-2">
{locales.map((locale, index) => (
<Button
key={index}
active={locale === router.locale}
href={href}
locale={locale}
>
{prettyLanguage(locale, props.languages)}
</Button>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,101 @@
import Button from "components/Button";
import Chip from "components/Chip";
import {
GetLibraryItemQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { prettyinlineTitle, prettySlug } from "queries/helpers";
import { useState } from "react";
type ContentTOCLineProps = {
content: GetLibraryItemQuery["libraryItems"]["data"][number]["attributes"]["contents"]["data"][number];
parentSlug: string;
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function ContentTOCLine(
props: ContentTOCLineProps
): JSX.Element {
const { content, langui, parentSlug } = props;
const [opened, setOpened] = useState(false);
return (
<div
className={`grid gap-2 px-4 rounded-lg ${
opened && "bg-mid shadow-inner-sm shadow-shade h-auto py-3 my-2"
}`}
>
<div className="grid gap-4 place-items-center grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]">
<a>
<h3 className="cursor-pointer" onClick={() => setOpened(!opened)}>
{content.attributes.content.data &&
content.attributes.content.data.attributes.titles.length > 0
? prettyinlineTitle(
content.attributes.content.data.attributes.titles[0]
.pre_title,
content.attributes.content.data.attributes.titles[0].title,
content.attributes.content.data.attributes.titles[0].subtitle
)
: prettySlug(content.attributes.slug, props.parentSlug)}
</h3>
</a>
<div className="flex flex-row flex-wrap gap-1">
{content.attributes.content.data?.attributes.categories.data.map(
(category) => (
<Chip key={category.id}>{category.attributes.short}</Chip>
)
)}
</div>
<p className="border-b-2 h-4 w-full border-black border-dotted opacity-30"></p>
<p>
{content.attributes.range[0].__typename === "ComponentRangePageRange"
? content.attributes.range[0].starting_page
: ""}
</p>
{content.attributes.content.data && (
<Chip className="justify-self-end thin:hidden">
{content.attributes.content.data.attributes.type.data.attributes
.titles.length > 0
? content.attributes.content.data.attributes.type.data.attributes
.titles[0].title
: prettySlug(
content.attributes.content.data.attributes.type.data
.attributes.slug
)}
</Chip>
)}
</div>
<div
className={`grid-flow-col place-content-start place-items-center gap-2 ${
opened ? "grid" : "hidden"
}`}
>
<span className="material-icons text-dark">
subdirectory_arrow_right
</span>
{content.attributes.scan_set.length > 0 && (
<Button
href={`/library/${parentSlug}/scans#${content.attributes.slug}`}
>
{langui.view_scans}
</Button>
)}
{content.attributes.content.data && (
<Button
href={`/contents/${content.attributes.content.data.attributes.slug}`}
>
{langui.open_content}
</Button>
)}
{content.attributes.scan_set.length === 0 &&
!content.attributes.content.data
? "The content is not available"
: ""}
</div>
</div>
);
}

View File

@ -1,8 +1,8 @@
import Link from "next/link";
import { GetContentsQuery } from "graphql/operations-types";
import { prettySlug } from "queries/helpers";
import Chip from "components/Chip";
import Img, { ImageQuality } from "components/Img";
import { GetContentsQuery } from "graphql/operations-types";
import Link from "next/link";
import { prettySlug } from "queries/helpers";
export type LibraryContentPreviewProps = {
item: {
@ -17,10 +17,10 @@ export type LibraryContentPreviewProps = {
export default function LibraryContentPreview(
props: LibraryContentPreviewProps
): JSX.Element {
const item = props.item;
const { item } = props;
return (
<Link href={"/contents/" + item.slug} passHref>
<Link href={`/contents/${item.slug}`} passHref>
<div className="drop-shadow-shade-xl cursor-pointer grid items-end fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02] transition-transform">
{item.thumbnail.data ? (
<Img
@ -33,14 +33,12 @@ export default function LibraryContentPreview(
)}
<div className="linearbg-obi fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md bottom-2 -inset-x-0.5 opacity-[var(--cover-opacity)] transition-opacity z-20 grid p-4 gap-2">
<div className="grid grid-flow-col gap-1 overflow-hidden place-content-start">
{item.type.data ? (
{item.type.data && (
<Chip>
{item.type.data.attributes.titles.length > 0
? item.type.data.attributes.titles[0].title
: prettySlug(item.type.data.attributes.slug)}
</Chip>
) : (
""
)}
</div>
<div>

View File

@ -1,8 +1,12 @@
import Link from "next/link";
import { GetLibraryItemsPreviewQuery } from "graphql/operations-types";
import { prettyDate, prettyPrice, prettyItemSubType } from "queries/helpers";
import Chip from "components/Chip";
import Img, { ImageQuality } from "components/Img";
import { useAppLayout } from "contexts/AppLayoutContext";
import {
GetCurrenciesQuery,
GetLibraryItemsPreviewQuery,
} from "graphql/operations-types";
import Link from "next/link";
import { prettyDate, prettyItemSubType, prettyPrice } from "queries/helpers";
export type LibraryItemsPreviewProps = {
className?: string;
@ -12,68 +16,76 @@ export type LibraryItemsPreviewProps = {
title: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["title"];
subtitle: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["subtitle"];
price?: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["price"];
categories: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["categories"];
release_date?: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["release_date"];
metadata?: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["metadata"];
};
currencies?: GetCurrenciesQuery["currencies"]["data"];
};
export default function LibraryItemsPreview(
props: LibraryItemsPreviewProps
): JSX.Element {
const item = props.item;
const { item } = props;
const appLayout = useAppLayout();
return (
<Link href={"/library/" + item.slug} passHref>
<Link href={`/library/${item.slug}`} passHref>
<div
className={`drop-shadow-shade-xl cursor-pointer grid items-end hover:rounded-3xl fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02] transition-transform ${props.className}`}
>
{item.thumbnail.data ? (
<Img
image={item.thumbnail.data.attributes}
quality={ImageQuality.Medium}
quality={ImageQuality.Small}
/>
) : (
<div className="w-full aspect-[21/29.7] bg-light rounded-lg"></div>
)}
<div className="linearbg-obi fine:drop-shadow-shade-lg fine:absolute place-items-start bottom-2 -inset-x-0.5 opacity-[var(--cover-opacity)] transition-opacity z-20 grid p-4 gap-2">
{item.metadata && item.metadata.length > 0 ? (
{item.metadata && item.metadata.length > 0 && (
<div className="flex flex-row gap-1">
<Chip>{prettyItemSubType(item.metadata[0])}</Chip>
</div>
) : (
""
)}
<div>
<h2 className="mobile:text-sm text-lg leading-5">{item.title}</h2>
<h3 className="mobile:text-xs leading-3">{item.subtitle}</h3>
</div>
{item.release_date || item.price ? (
<div className="w-full grid grid-flow-col gap-1 overflow-x-scroll webkit-scrollbar:h-0 [scrollbar-width:none] place-content-start">
{item.categories.data.map((category) => (
<Chip key={category.id} className="text-sm">
{category.attributes.short}
</Chip>
))}
</div>
{(item.release_date || item.price) && (
<div className="grid grid-flow-col w-full">
{item.release_date ? (
{item.release_date && (
<p className="mobile:text-xs text-sm">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
event
</span>
{prettyDate(item.release_date)}
</p>
) : (
""
)}
{item.price ? (
{item.price && props.currencies && (
<p className="mobile:text-xs text-sm justify-self-end">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
shopping_cart
</span>
{prettyPrice(item.price)}
{prettyPrice(
item.price,
props.currencies,
appLayout.currency
)}
</p>
) : (
""
)}
</div>
) : (
""
)}
</div>
</div>

View File

@ -0,0 +1,39 @@
import { useMediaMobile } from "hooks/useMediaQuery";
import { Dispatch, SetStateAction } from "react";
import Lightbox from "react-image-lightbox";
export type LightBoxProps = {
setState:
| Dispatch<SetStateAction<boolean | undefined>>
| Dispatch<SetStateAction<boolean>>;
state: boolean;
images: string[];
index: number;
setIndex: Dispatch<SetStateAction<number>>;
};
export default function LightBox(props: LightBoxProps): JSX.Element {
const { state, setState, images, index, setIndex } = props;
const mobile = useMediaMobile();
return (
<>
{state && (
<Lightbox
reactModalProps={{
parentSelector: () => document.getElementById("MyAppLayout"),
}}
mainSrc={images[index]}
prevSrc={index > 0 ? images[index - 1] : undefined}
nextSrc={index < images.length ? images[index + 1] : undefined}
onMovePrevRequest={() => setIndex(index - 1)}
onMoveNextRequest={() => setIndex(index + 1)}
imageCaption=""
imageTitle=""
onCloseRequest={() => setState(false)}
imagePadding={mobile ? 0 : 70}
/>
)}
</>
);
}

View File

@ -1,30 +1,388 @@
import HorizontalLine from "components/HorizontalLine";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import InsetBox from "components/InsetBox";
import LightBox from "components/LightBox";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import Markdown from "markdown-to-jsx";
import SceneBreak from "./SceneBreak";
import { useRouter } from "next/router";
import { slugify } from "queries/helpers";
import React, { useState } from "react";
import ReactDOMServer from "react-dom/server";
type ScenBreakProps = {
type MarkdawnProps = {
className?: string;
text: string;
};
export default function Markdawn(props: ScenBreakProps): JSX.Element {
if (props.text) {
export default function Markdawn(props: MarkdawnProps): JSX.Element {
const appLayout = useAppLayout();
const text = preprocessMarkDawn(props.text);
const router = useRouter();
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
if (text) {
return (
<Markdown
className={`prose prose-p:text-justify text-black ${props.className}`}
options={{
overrides: {
hr: {
component: SceneBreak,
<>
<LightBox
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<Markdown
className={`formatted ${props.className}`}
options={{
slugify: slugify,
overrides: {
a: {
component: (compProps: {
href: string;
children: React.ReactNode;
}) => {
if (compProps.href.startsWith("/")) {
return (
<a onClick={async () => router.push(compProps.href)}>
{compProps.children}
</a>
);
}
return <a href={compProps.href}>{compProps.children}</a>;
},
},
h1: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h1 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h1>
),
},
h2: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h2 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h2>
),
},
h3: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h3 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h3>
),
},
h4: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h4 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h4>
),
},
h5: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h5 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h5>
),
},
h6: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h6 id={compProps.id} style={compProps.style}>
{compProps.children}
<HeaderToolTip id={compProps.id} />
</h6>
),
},
Sep: {
component: () => <div className="my-24"></div>,
},
SceneBreak: {
component: (compProps: { id: string }) => (
<div
id={compProps.id}
className={"h-0 text-center text-3xl text-dark mt-16 mb-20"}
>
* * *
</div>
),
},
IntraLink: {
component: (compProps: {
children: React.ReactNode;
target?: string;
page?: string;
}) => {
const slug = compProps.target
? slugify(compProps.target)
: slugify(compProps.children?.toString());
return (
<a
onClick={async () =>
router.replace(
`${compProps.page ? compProps.page : ""}#${slug}`
)
}
>
{compProps.children}
</a>
);
},
},
player: {
component: () => (
<span className="text-dark opacity-70">
{appLayout.playerName ? appLayout.playerName : "<player>"}
</span>
),
},
Transcript: {
component: (compProps) => (
<div className="grid grid-cols-[auto_1fr] mobile:grid-cols-1 gap-x-6 gap-y-2">
{compProps.children}
</div>
),
},
Line: {
component: (compProps) => (
<>
<strong className="text-dark opacity-60 mobile:!-mb-4">
{compProps.name}
</strong>
<p className="whitespace-pre-line">{compProps.children}</p>
</>
),
},
InsetBox: {
component: (compProps) => (
<InsetBox className="my-12">{compProps.children}</InsetBox>
),
},
li: {
component: (compProps: { children: React.ReactNode }) => (
<li
className={
compProps.children &&
ReactDOMServer.renderToStaticMarkup(
<>{compProps.children}</>
).length > 100
? "my-4"
: ""
}
>
{compProps.children}
</li>
),
},
Highlight: {
component: (compProps: { children: React.ReactNode }) => (
<mark>{compProps.children}</mark>
),
},
footer: {
component: (compProps: { children: React.ReactNode }) => (
<>
<HorizontalLine />
<div>{compProps.children}</div>
</>
),
},
blockquote: {
component: (compProps: {
children: React.ReactNode;
cite?: string;
}) => (
<blockquote>
{compProps.cite ? (
<>
&ldquo;{compProps.children}&rdquo;
<cite> {compProps.cite}</cite>
</>
) : (
compProps.children
)}
</blockquote>
),
},
img: {
component: (compProps: {
alt: string;
src: string;
width?: number;
height?: number;
caption?: string;
name?: string;
}) => (
<div
className="my-8 cursor-pointer"
onClick={() => {
setLightboxOpen(true);
setLightboxImages([
compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Large)
: compProps.src,
]);
setLightboxIndex(0);
}}
>
{compProps.src.startsWith("/uploads/") ? (
<div className="relative w-full aspect-video">
<Img
image={{
__typename: "UploadFile",
alternativeText: compProps.alt,
url: compProps.src,
width: compProps.width ?? 1500,
height: compProps.height ?? 1000,
caption: compProps.caption ?? "",
name: compProps.name ?? "",
}}
layout="fill"
objectFit="contain"
quality={ImageQuality.Medium}
></Img>
</div>
) : (
<div className="grid place-content-center">
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<img {...compProps} className="max-h-[50vh] " />
</div>
)}
</div>
),
},
},
player: {
component: () => {return <span className="text-dark opacity-70">{"<player>"}</span>}
},
},
}}
>
{props.text}
</Markdown>
}}
>
{text}
</Markdown>
</>
);
}
return <></>;
}
}
function HeaderToolTip(props: { id: string }) {
return (
<ToolTip
content={"Copy anchor link"}
trigger="mouseenter"
className="text-sm"
>
<ToolTip content={"Copied! 👍"} trigger="click" className="text-sm">
<span
className="material-icons transition-color hover:text-dark cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(
`${process.env.NEXT_PUBLIC_URL_SELF + window.location.pathname}#${
props.id
}`
);
}}
>
link
</span>
</ToolTip>
</ToolTip>
);
}
export function preprocessMarkDawn(text: string): string {
if (!text) return "";
let scenebreakIndex = 0;
const visitedSlugs: string[] = [];
const result = text.split("\n").map((line) => {
if (line === "* * *" || line === "---") {
scenebreakIndex += 1;
return `<SceneBreak id="scene-break-${scenebreakIndex}">`;
}
if (line.startsWith("# ")) {
return markdawnHeadersParser(headerLevels.h1, line, visitedSlugs);
}
if (line.startsWith("## ")) {
return markdawnHeadersParser(headerLevels.h2, line, visitedSlugs);
}
if (line.startsWith("### ")) {
return markdawnHeadersParser(headerLevels.h3, line, visitedSlugs);
}
if (line.startsWith("#### ")) {
return markdawnHeadersParser(headerLevels.h4, line, visitedSlugs);
}
if (line.startsWith("##### ")) {
return markdawnHeadersParser(headerLevels.h5, line, visitedSlugs);
}
if (line.startsWith("###### ")) {
return markdawnHeadersParser(headerLevels.h6, line, visitedSlugs);
}
return line;
});
return result.join("\n");
}
enum headerLevels {
h1 = 1,
h2 = 2,
h3 = 3,
h4 = 4,
h5 = 5,
h6 = 6,
}
function markdawnHeadersParser(
headerLevel: headerLevels,
line: string,
visitedSlugs: string[]
): string {
const lineText = line.slice(headerLevel + 1);
const slug = slugify(lineText);
let newSlug = slug;
let index = 2;
while (visitedSlugs.includes(newSlug)) {
newSlug = `${slug}-${index}`;
index += 1;
}
visitedSlugs.push(newSlug);
return `<${headerLevels[headerLevel]} id="${newSlug}">${lineText}</${headerLevels[headerLevel]}>`;
}

View File

@ -1,15 +0,0 @@
type ScenBreakProps = {
className?: string;
};
export default function SceneBreak(props: ScenBreakProps): JSX.Element {
return (
<div
className={
"h-0 text-center text-3xl text-dark mt-16 mb-20" + " " + props.className
}
>
* * *
</div>
);
}

View File

@ -0,0 +1,165 @@
import { useRouter } from "next/router";
import { slugify } from "queries/helpers";
import { preprocessMarkDawn } from "./Markdawn";
type TOCProps = {
text: string;
title?: string;
};
export default function TOCComponent(props: TOCProps): JSX.Element {
const { text, title } = props;
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
const router = useRouter();
return (
<>
<h3 className="text-xl">Table of content</h3>
<div className="text-left max-w-[14.5rem]">
<p className="my-2 overflow-x-hidden relative text-ellipsis whitespace-nowrap text-left">
<a className="" onClick={async () => router.replace(`#${toc.slug}`)}>
{<abbr title={toc.title}>{toc.title}</abbr>}
</a>
</p>
<TOCLevel tocchildren={toc.children} parentNumbering="" />
</div>
</>
);
}
type TOCLevelProps = {
tocchildren: TOC[];
parentNumbering: string;
};
function TOCLevel(props: TOCLevelProps): JSX.Element {
const router = useRouter();
const { tocchildren, parentNumbering } = props;
return (
<ol className="pl-4 text-left">
{tocchildren.map((child, childIndex) => (
<>
<li
key={child.slug}
className="my-2 overflow-x-hidden w-full text-ellipsis whitespace-nowrap"
>
<span className="text-dark">{`${parentNumbering}${
childIndex + 1
}.`}</span>{" "}
<a onClick={async () => router.replace(`#${child.slug}`)}>
{<abbr title={child.title}>{child.title}</abbr>}
</a>
</li>
<TOCLevel
tocchildren={child.children}
parentNumbering={`${parentNumbering}${childIndex + 1}.`}
/>
</>
))}
</ol>
);
}
export type TOC = {
title: string;
slug: string;
children: TOC[];
};
export function getTocFromMarkdawn(text: string, title?: string): TOC {
const toc: TOC = {
title: title ?? "Return to top",
slug: slugify(title) ?? "",
children: [],
};
let h2 = -1;
let h3 = -1;
let h4 = -1;
let h5 = -1;
let scenebreak = 0;
let scenebreakIndex = 0;
function getTitle(line: string): string {
return line.slice(line.indexOf(`">`) + 2, line.indexOf("</"));
}
function getSlug(line: string): string {
return line.slice(line.indexOf(`id="`) + 4, line.indexOf(`">`));
}
text.split("\n").map((line) => {
if (line.startsWith("<h1 id=")) {
toc.title = getTitle(line);
toc.slug = getSlug(line);
} else if (line.startsWith("<h2 id=")) {
toc.children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
});
h2 += 1;
h3 = -1;
h4 = -1;
h5 = -1;
scenebreak = 0;
} else if (line.startsWith("<h3 id=")) {
toc.children[h2].children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
});
h3 += 1;
h4 = -1;
h5 = -1;
scenebreak = 0;
} else if (line.startsWith("<h4 id=")) {
toc.children[h2].children[h3].children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
});
h4 += 1;
h5 = -1;
scenebreak = 0;
} else if (line.startsWith("<h5 id=")) {
toc.children[h2].children[h3].children[h4].children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
});
h5 += 1;
scenebreak = 0;
} else if (line.startsWith("<h6 id=")) {
toc.children[h2].children[h3].children[h4].children[h5].children.push({
title: getTitle(line),
slug: getSlug(line),
children: [],
});
} else if (line.startsWith(`<SceneBreak`)) {
scenebreak += 1;
scenebreakIndex += 1;
const child = {
title: `Scene break ${scenebreak}`,
slug: slugify(`scene-break-${scenebreakIndex}`),
children: [],
};
if (h5 >= 0) {
toc.children[h2].children[h3].children[h4].children[h5].children.push(
child
);
} else if (h4 >= 0) {
toc.children[h2].children[h3].children[h4].children.push(child);
} else if (h3 >= 0) {
toc.children[h2].children[h3].children.push(child);
} else if (h2 >= 0) {
toc.children[h2].children.push(child);
} else {
toc.children.push(child);
}
}
});
return toc;
}

View File

@ -0,0 +1,64 @@
import Chip from "components/Chip";
import Img, { ImageQuality } from "components/Img";
import { GetPostsPreviewQuery } from "graphql/operations-types";
import Link from "next/link";
import { prettyDate, prettySlug } from "queries/helpers";
export type PostPreviewProps = {
post: {
slug: GetPostsPreviewQuery["posts"]["data"][number]["attributes"]["slug"];
thumbnail: GetPostsPreviewQuery["posts"]["data"][number]["attributes"]["thumbnail"];
translations: GetPostsPreviewQuery["posts"]["data"][number]["attributes"]["translations"];
categories: GetPostsPreviewQuery["posts"]["data"][number]["attributes"]["categories"];
date: GetPostsPreviewQuery["posts"]["data"][number]["attributes"]["date"];
};
};
export default function PostPreview(props: PostPreviewProps): JSX.Element {
const { post } = props;
return (
<Link href={`/news/${post.slug}`} passHref>
<div className="drop-shadow-shade-xl cursor-pointer grid items-end hover:scale-[1.02] transition-transform">
{post.thumbnail.data ? (
<Img
className="rounded-md rounded-b-none"
image={post.thumbnail.data.attributes}
quality={ImageQuality.Medium}
/>
) : (
<div className="w-full aspect-[3/2] bg-light rounded-lg"></div>
)}
<div className="linearbg-obi fine:drop-shadow-shade-lg rounded-b-md top-full transition-opacity z-20 grid p-4 gap-2">
<div className="grid grid-flow-col w-full">
{post.date && (
<p className="mobile:text-xs text-sm">
<span className="material-icons !text-base translate-y-[.15em] mr-1">
event
</span>
{prettyDate(post.date)}
</p>
)}
</div>
<div>
{post.translations.length > 0 ? (
<>
<h1 className="text-xl">{post.translations[0].title}</h1>
<p>{post.translations[0].excerpt}</p>
</>
) : (
<h1 className="text-lg">{prettySlug(post.slug)}</h1>
)}
</div>
<div className="grid grid-flow-col gap-1 overflow-x-scroll webkit-scrollbar:w-0 [scrollbar-width:none] place-content-start">
{post.categories.data.map((category) => (
<Chip key={category.id} className="text-sm">
{category.attributes.short}
</Chip>
))}
</div>
</div>
</div>
</Link>
);
}

View File

@ -1,14 +1,12 @@
import ToolTip from "components/ToolTip";
import { useRouter } from "next/router";
import Link from "next/link";
import { MouseEventHandler } from "react";
import ReactDOMServer from "react-dom/server";
type NavOptionProps = {
url: string;
icon?: string;
title: string;
subtitle?: string;
tooltipId?: string;
border?: boolean;
reduced?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
@ -25,21 +23,29 @@ export default function NavOption(props: NavOptionProps): JSX.Element {
} ${isActive ? divActive : ""}`;
return (
<Link href={props.url} passHref>
<ToolTip
content={
<div>
<h3 className="text-2xl">{props.title}</h3>
{props.subtitle && <p className="col-start-2">{props.subtitle}</p>}
</div>
}
placement="right"
className="text-left"
disabled={!props.reduced}
>
<div
onClick={props.onClick}
data-html
data-multiline
data-tip={ReactDOMServer.renderToStaticMarkup(
<div className="px-4 py-3">
<h3 className="text-2xl">{props.title}</h3>
{props.subtitle && (
<p className="max-w-[10rem]">{props.subtitle}</p>
)}
</div>
)}
data-for={props.tooltipId}
className={`grid grid-flow-col grid-cols-[auto] auto-cols-fr justify-center ${
onClick={(event) => {
if (props.onClick) props.onClick(event);
if (props.url) {
if (props.url.startsWith("#")) {
router.replace(props.url);
} else {
router.push(props.url);
}
}
}}
className={`relative grid grid-flow-col grid-cols-[auto] auto-cols-fr justify-center ${
props.icon ? "text-left" : "text-center"
} ${divCommon}`}
>
@ -54,6 +60,6 @@ export default function NavOption(props: NavOptionProps): JSX.Element {
</div>
)}
</div>
</Link>
</ToolTip>
);
}

View File

@ -10,10 +10,8 @@ export default function PanelHeader(props: PanelHeaderProps): JSX.Element {
return (
<>
<div className="w-full grid place-items-center">
{props.icon ? (
{props.icon && (
<span className="material-icons !text-4xl mb-3">{props.icon}</span>
) : (
""
)}
<h2 className="text-2xl">{props.title}</h2>
{props.description ? <p>{props.description}</p> : ""}

View File

@ -1,4 +1,5 @@
import Button from "components/Button";
import HorizontalLine from "components/HorizontalLine";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
@ -6,14 +7,39 @@ type ReturnButtonProps = {
href: string;
title: string;
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
displayOn: ReturnButtonType;
horizontalLine?: boolean;
className?: string;
};
export enum ReturnButtonType {
mobile = "mobile",
desktop = "desktop",
both = "both",
}
export default function ReturnButton(props: ReturnButtonProps): JSX.Element {
const appLayout = useAppLayout();
return (
<Button onClick={() => appLayout.setSubPanelOpen(false)} href={props.href}>
&emsp;{props.langui.global_return_label} {props.title}
</Button>
<div
className={`${
props.displayOn === ReturnButtonType.mobile
? "desktop:hidden"
: props.displayOn === ReturnButtonType.desktop
? "mobile:hidden"
: ""
} ${props.className}`}
>
<Button
onClick={() => appLayout.setSubPanelOpen(false)}
href={props.href}
className="grid grid-flow-col gap-2"
>
<span className="material-icons">navigate_before</span>
{props.langui.return_to} {props.title}
</Button>
{props.horizontalLine && <HorizontalLine />}
</div>
);
}

View File

@ -5,19 +5,22 @@ type ContentPanelProps = {
};
export enum ContentPanelWidthSizes {
default,
large,
default = "default",
large = "large",
}
export default function ContentPanel(props: ContentPanelProps): JSX.Element {
const width = props.width ? props.width : ContentPanelWidthSizes.default;
const widthCSS =
width === ContentPanelWidthSizes.default ? "max-w-[45rem]" : "w-full";
const prose = props.autoformat ? "prose text-justify" : "";
width === ContentPanelWidthSizes.default ? "max-w-2xl" : "w-full";
return (
<div className={`grid pt-10 pb-20 px-6 desktop:py-20 desktop:px-10`}>
<main className={`${prose} ${widthCSS} place-self-center`}>
<main
className={`${
props.autoformat && "formatted"
} ${widthCSS} place-self-center`}
>
{props.children}
</main>
</div>

View File

@ -1,20 +1,20 @@
import Link from "next/link";
import NavOption from "components/PanelComponents/NavOption";
import SVG from "components/SVG";
import { useRouter } from "next/router";
import Button from "components/Button";
import HorizontalLine from "components/HorizontalLine";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import Markdown from "markdown-to-jsx";
import { useMediaDesktop } from "hooks/useMediaQuery";
import NavOption from "components/PanelComponents/NavOption";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { useMediaDesktop } from "hooks/useMediaQuery";
import Markdown from "markdown-to-jsx";
import Link from "next/link";
import { useRouter } from "next/router";
type MainPanelProps = {
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function MainPanel(props: MainPanelProps): JSX.Element {
const langui = props.langui;
const { langui } = props;
const router = useRouter();
const isDesktop = useMediaDesktop();
const appLayout = useAppLayout();
@ -25,6 +25,20 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
appLayout.mainPanelReduced && isDesktop && "px-4"
}`}
>
{/* Reduce/expand main menu */}
<div
className={`mobile:hidden top-1/2 fixed ${
appLayout.mainPanelReduced ? "left-[4.65rem]" : "left-[18.65rem]"
}`}
onClick={() =>
appLayout.setMainPanelReduced(!appLayout.mainPanelReduced)
}
>
<Button className="material-icons bg-light !px-2">
{appLayout.mainPanelReduced ? "chevron_right" : "chevron_left"}
</Button>
</div>
<div>
<div className="grid place-items-center">
<Link href="/" passHref>
@ -49,32 +63,74 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
: "flex-row"
} flex-wrap gap-2`}
>
<Button
onClick={() => {
appLayout.setDarkMode(!appLayout.darkMode);
appLayout.setSelectedThemeMode(true);
}}
className={
appLayout.mainPanelReduced && isDesktop ? "" : "!py-0.5 !px-2.5"
}
<ToolTip
content={<h3 className="text-2xl">{langui.open_settings}</h3>}
placement="right"
className="text-left"
disabled={!appLayout.mainPanelReduced}
>
<span className="material-icons !text-sm">
{appLayout.darkMode ? "dark_mode" : "light_mode"}
</span>
</Button>
{router.locale && (
<Button
onClick={() => appLayout.setLanguagePanelOpen(true)}
className={
appLayout.mainPanelReduced && isDesktop
? ""
: "!py-0.5 !px-2.5 !text-sm"
: "!py-0.5 !px-2.5"
}
onClick={() => {
appLayout.setConfigPanelOpen(true);
}}
>
<span
className={`material-icons ${
!(appLayout.mainPanelReduced && isDesktop) && "!text-sm"
} `}
>
settings
</span>
</Button>
</ToolTip>
{router.locale && (
<ToolTip
content={<h3 className="text-2xl">{langui.change_language}</h3>}
placement="right"
className="text-left"
disabled={!appLayout.mainPanelReduced}
>
<Button
onClick={() => appLayout.setLanguagePanelOpen(true)}
className={
appLayout.mainPanelReduced && isDesktop
? ""
: "!py-0.5 !px-2.5 !text-sm"
}
>
{router.locale.toUpperCase()}
</Button>
</ToolTip>
)}
{/* <ToolTip
content={<h3 className="text-2xl">{langui.open_search}</h3>}
placement="right"
className="text-left"
disabled={!appLayout.mainPanelReduced}
>
<Button
className={
appLayout.mainPanelReduced && isDesktop
? ""
: "!py-0.5 !px-2.5"
}
>
{router.locale.toUpperCase()}
<span
className={`material-icons ${
!(appLayout.mainPanelReduced && isDesktop) && "!text-sm"
} `}
>
search
</span>
</Button>
)}
</ToolTip> */}
</div>
</div>
</div>
@ -84,9 +140,8 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
<NavOption
url="/library"
icon="library_books"
title={langui.main_library}
subtitle={langui.main_library_description}
tooltipId="MainPanelTooltip"
title={langui.library}
subtitle={langui.library_short_description}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
@ -94,9 +149,8 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
<NavOption
url="/contents"
icon="workspaces"
title="Contents"
subtitle="Explore all content and filter by type or category"
tooltipId="MainPanelTooltip"
title={langui.contents}
subtitle={langui.contents_short_description}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
@ -104,66 +158,73 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
<NavOption
url="/wiki"
icon="travel_explore"
title={langui.main_wiki}
subtitle={langui.main_wiki_description}
tooltipId="MainPanelTooltip"
title={langui.wiki}
subtitle={langui.wiki_short_description}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
{/*
<NavOption
url="/chronicles"
icon="watch_later"
title={langui.main_chronicles}
subtitle={langui.main_chronicles_description}
tooltipId="MainPanelTooltip"
title={langui.chronicles}
subtitle={langui.chronicles_short_description}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
*/}
<HorizontalLine />
<NavOption
url="/news"
icon="feed"
title={langui.main_news}
tooltipId="MainPanelTooltip"
title={langui.news}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
{/*
<NavOption
url="/merch"
icon="store"
title={langui.main_merch}
tooltipId="MainPanelTooltip"
title={langui.merch}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
*/}
<NavOption
url="/gallery"
icon="collections"
title={langui.main_gallery}
tooltipId="MainPanelTooltip"
title={langui.gallery}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
{/*
<NavOption
url="/archives"
icon="inventory"
title={langui.main_archives}
tooltipId="MainPanelTooltip"
title={langui.archives}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
*/}
<NavOption
url="/about-us"
icon="info"
title={langui.main_about_us}
tooltipId="MainPanelTooltip"
title={langui.about_us}
reduced={appLayout.mainPanelReduced && isDesktop}
onClick={() => appLayout.setMainPanelOpen(false)}
/>
@ -176,10 +237,8 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
}`}
>
<p>
{langui.main_licensing ? (
<Markdown>{langui.main_licensing}</Markdown>
) : (
""
{langui.licensing_notice && (
<Markdown>{langui.licensing_notice}</Markdown>
)}
</p>
<a
@ -194,10 +253,8 @@ export default function MainPanel(props: MainPanelProps): JSX.Element {
</div>
</a>
<p>
{langui.main_copyright ? (
<Markdown>{langui.main_copyright}</Markdown>
) : (
""
{langui.copyright_notice && (
<Markdown>{langui.copyright_notice}</Markdown>
)}
</p>
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">

View File

@ -4,7 +4,7 @@ type SubPanelProps = {
export default function SubPanel(props: SubPanelProps): JSX.Element {
return (
<div className="flex flex-col p-8 gap-y-2 justify-items-center text-center mobile:h-full">
<div className="grid pt-10 pb-20 px-6 desktop:py-8 desktop:px-10 gap-y-2 text-center">
{props.children}
</div>
);

54
src/components/Popup.tsx Normal file
View File

@ -0,0 +1,54 @@
import { Dispatch, SetStateAction } from "react";
import Button from "./Button";
export type PopupProps = {
setState:
| Dispatch<SetStateAction<boolean | undefined>>
| Dispatch<SetStateAction<boolean>>;
state?: boolean;
children: React.ReactNode;
fillViewport?: boolean;
hideBackground?: boolean;
};
export default function Popup(props: PopupProps): JSX.Element {
return (
<div
className={`fixed inset-0 z-50 grid place-content-center transition-[backdrop-filter] duration-500 ${
props.state
? "[backdrop-filter:blur(2px)]"
: "pointer-events-none touch-none"
}`}
onKeyUp={(event) => {
if (event.key.match("Escape")) props.setState(false);
}}
tabIndex={0}
>
<div
className={`fixed bg-shade inset-0 transition-all duration-500 ${
props.state ? "bg-opacity-50" : "bg-opacity-0"
}`}
onClick={() => {
props.setState(false);
}}
/>
<div
className={`p-10 grid gap-4 place-items-center transition-transform ${
props.state ? "scale-100" : "scale-0"
} ${props.fillViewport ? "absolute inset-10 top-20" : "relative"} ${
props.hideBackground
? ""
: "bg-light rounded-lg shadow-2xl shadow-shade"
}`}
>
<Button
className="!p-1 absolute -top-16 bg-light border-light border-4"
onClick={() => props.setState(false)}
>
<span className="material-icons p-1">close</span>
</Button>
{props.children}
</div>
</div>
);
}

View File

@ -1,60 +1,69 @@
import Chip from "components/Chip";
import { GetContentTextQuery } from "graphql/operations-types";
import {
GetContentTextQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import Button from "./Button";
import Img, { ImageQuality } from "./Img";
import ReactDOMServer from "react-dom/server";
import ToolTip from "./ToolTip";
type RecorderChipProps = {
className?: string;
recorder: GetContentTextQuery["contents"]["data"][number]["attributes"]["text_set"][number]["transcribers"]["data"][number];
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
};
export default function RecorderChip(props: RecorderChipProps): JSX.Element {
const recorder = props.recorder;
const { recorder, langui } = props;
return (
<Chip
key={recorder.id}
data-tip={ReactDOMServer.renderToStaticMarkup(
<div className="p-2 py-5 grid gap-2">
<div className="grid grid-flow-col gap-2 place-items-center place-content-start">
<ToolTip
content={
<div className="text-left p-2 py-5 grid gap-8">
<div className="grid grid-flow-col gap-6 place-items-center place-content-start">
{recorder.attributes.avatar.data && (
<Img
className="w-8 rounded-full"
className="w-20 rounded-full border-4 border-mid"
image={recorder.attributes.avatar.data.attributes}
quality={ImageQuality.Small}
rawImg
/>
)}
<h3 className="text-xl">{recorder.attributes.username}</h3>
<div className="grid gap-2">
<h3 className=" text-2xl">{recorder.attributes.username}</h3>
{recorder.attributes.languages.data.length > 0 && (
<div className="flex flex-row flex-wrap gap-1">
<p>{langui.languages}:</p>
{recorder.attributes.languages.data.map((language) => (
<Chip key={language.attributes.code}>
{language.attributes.code.toUpperCase()}
</Chip>
))}
</div>
)}
{recorder.attributes.pronouns && (
<div className="flex flex-row flex-wrap gap-1">
<p>{langui.pronouns}:</p>
<Chip>{recorder.attributes.pronouns}</Chip>
</div>
)}
</div>
</div>
{recorder.attributes.languages.data.length > 0 && (
<div className="flex flex-row flex-wrap gap-1">
<p>Languages:</p>
{recorder.attributes.languages.data.map((language) => (
<Chip key={language.attributes.code}>
{language.attributes.code.toUpperCase()}
</Chip>
))}
</div>
{recorder.attributes.bio.length > 0 && (
<p>{recorder.attributes.bio[0].bio}</p>
)}
{recorder.attributes.pronouns && (
<div className="flex flex-row flex-wrap gap-1">
<p>Pronouns:</p>
<Chip>{recorder.attributes.pronouns}</Chip>
</div>
)}
<p>
{recorder.attributes.bio.length > 0 &&
recorder.attributes.bio[0].bio}
</p>
<Button className="cursor-not-allowed">View profile</Button>
</div>
)}
data-for={"RecordersTooltip"}
data-multiline
data-html
}
placement="top"
>
{recorder.attributes.anonymize
? `Recorder#${recorder.attributes.anonymous_code}`
: recorder.attributes.username}
</Chip>
<Chip key={recorder.id}>
{recorder.attributes.anonymize
? `Recorder#${recorder.attributes.anonymous_code}`
: recorder.attributes.username}
</Chip>
</ToolTip>
);
}

66
src/components/Select.tsx Normal file
View File

@ -0,0 +1,66 @@
import { Dispatch, SetStateAction, useState } from "react";
export type SelectProps = {
setState: Dispatch<SetStateAction<number>>;
state: number;
options: string[];
selected?: number;
allowEmpty?: boolean;
className?: string;
};
export default function Select(props: SelectProps): JSX.Element {
const [opened, setOpened] = useState(false);
return (
<div
className={`relative text-center transition-[filter] ${
opened && "drop-shadow-shade-lg z-10"
} ${props.className}`}
>
<div
className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center cursor-pointer hover:bg-mid transition-all ${
opened && "outline-[transparent] rounded-b-none"
}`}
>
<p onClick={() => setOpened(!opened)} className="w-full">
{props.state === -1 ? "—" : props.options[props.state]}
</p>
{props.state >= 0 && props.allowEmpty && (
<span
onClick={() => props.setState(-1)}
className="material-icons !text-xs"
>
close
</span>
)}
<span onClick={() => setOpened(!opened)} className="material-icons">
{opened ? "arrow_drop_up" : "arrow_drop_down"}
</span>
</div>
<div
className={`left-0 right-0 rounded-b-[1em] ${
opened ? "absolute" : "hidden"
}`}
>
{props.options.map((option, index) => (
<>
{index !== props.state && (
<div
className="bg-light hover:bg-mid transition-colors cursor-pointer p-1 last-of-type:rounded-b-[1em]"
key={index}
id={option}
onClick={() => {
setOpened(false);
props.setState(index);
}}
>
{option}
</div>
)}
</>
))}
</div>
</div>
);
}

26
src/components/Switch.tsx Normal file
View File

@ -0,0 +1,26 @@
import { Dispatch, SetStateAction } from "react";
export type SwitchProps = {
setState: Dispatch<SetStateAction<boolean>>;
state: boolean;
className?: string;
};
export default function Switch(props: SwitchProps): JSX.Element {
return (
<div
className={`h-6 w-12 rounded-full border-2 border-mid grid transition-colors relative cursor-pointer ${
props.className
} ${props.state ? "bg-mid" : "bg-light"}`}
onClick={() => {
props.setState(!props.state);
}}
>
<div
className={`bg-dark aspect-square rounded-full absolute top-0 bottom-0 left-0 transition-transform ${
props.state && "translate-x-[115%]"
}`}
></div>
</div>
);
}

View File

@ -0,0 +1,19 @@
import Tippy, { TippyProps } from "@tippyjs/react";
import "tippy.js/animations/scale-subtle.css";
interface ToolTipProps extends TippyProps {}
export default function ToolTip(props: ToolTipProps): JSX.Element {
const newProps = { ...props };
// Set defaults
if (newProps.delay === undefined) newProps.delay = [150, 0];
if (newProps.interactive === undefined) newProps.interactive = true;
if (newProps.animation === undefined) newProps.animation = "scale-subtle";
return (
<Tippy className={`text-[80%] ${newProps.className}`} {...newProps}>
<div>{props.children}</div>
</Tippy>
);
}

View File

@ -5,13 +5,20 @@ import React, { ReactNode, useContext } from "react";
export interface AppLayoutState {
subPanelOpen: boolean | undefined;
languagePanelOpen: boolean | undefined;
configPanelOpen: boolean | undefined;
mainPanelReduced: boolean | undefined;
mainPanelOpen: boolean | undefined;
darkMode: boolean | undefined;
selectedThemeMode: boolean | undefined;
fontSize: number | undefined;
dyslexic: boolean | undefined;
currency: string | undefined;
playerName: string | undefined;
setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setLanguagePanelOpen: React.Dispatch<
React.SetStateAction<boolean | undefined>
>;
setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setMainPanelReduced: React.Dispatch<
React.SetStateAction<boolean | undefined>
>;
@ -20,27 +27,44 @@ export interface AppLayoutState {
setSelectedThemeMode: React.Dispatch<
React.SetStateAction<boolean | undefined>
>;
setFontSize: React.Dispatch<React.SetStateAction<number | undefined>>;
setDyslexic: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setCurrency: React.Dispatch<React.SetStateAction<string | undefined>>;
setPlayerName: React.Dispatch<React.SetStateAction<string | undefined>>;
}
/* eslint-disable @typescript-eslint/no-empty-function */
const initialState: AppLayoutState = {
subPanelOpen: false,
languagePanelOpen: false,
configPanelOpen: false,
mainPanelReduced: false,
mainPanelOpen: false,
darkMode: false,
selectedThemeMode: false,
fontSize: 1,
dyslexic: false,
currency: "USD",
playerName: "",
setSubPanelOpen: () => {},
setLanguagePanelOpen: () => {},
setMainPanelReduced: () => {},
setMainPanelOpen: () => {},
setDarkMode: () => {},
setSelectedThemeMode: () => {},
setConfigPanelOpen: () => {},
setFontSize: () => {},
setDyslexic: () => {},
setCurrency: () => {},
setPlayerName: () => {},
};
/* eslint-enable @typescript-eslint/no-empty-function */
const AppContext = React.createContext<AppLayoutState>(initialState);
export default AppContext;
export function useAppLayout() {
export function useAppLayout(): AppLayoutState {
return useContext(AppContext);
}
@ -48,7 +72,7 @@ type Props = {
children: ReactNode;
};
export const AppContextProvider = (props: Props) => {
export function AppContextProvider(props: Props): JSX.Element {
const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage<
boolean | undefined
>("subPanelOpen", initialState.subPanelOpen);
@ -57,6 +81,10 @@ export const AppContextProvider = (props: Props) => {
boolean | undefined
>("languagePanelOpen", initialState.languagePanelOpen);
const [configPanelOpen, setConfigPanelOpen] = useStateWithLocalStorage<
boolean | undefined
>("configPanelOpen", initialState.configPanelOpen);
const [mainPanelReduced, setMainPanelReduced] = useStateWithLocalStorage<
boolean | undefined
>("mainPanelReduced", initialState.mainPanelReduced);
@ -65,28 +93,56 @@ export const AppContextProvider = (props: Props) => {
boolean | undefined
>("mainPanelOpen", initialState.mainPanelOpen);
const [darkMode, setDarkMode, setSelectedThemeMode] = useDarkMode(
"darkMode",
initialState.darkMode
const [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode] =
useDarkMode("darkMode", initialState.darkMode);
const [fontSize, setFontSize] = useStateWithLocalStorage<number | undefined>(
"fontSize",
initialState.fontSize
);
const [dyslexic, setDyslexic] = useStateWithLocalStorage<boolean | undefined>(
"dyslexic",
initialState.dyslexic
);
const [currency, setCurrency] = useStateWithLocalStorage<string | undefined>(
"currency",
initialState.currency
);
const [playerName, setPlayerName] = useStateWithLocalStorage<
string | undefined
>("playerName", initialState.playerName);
return (
<AppContext.Provider
value={{
subPanelOpen,
languagePanelOpen,
configPanelOpen,
mainPanelReduced,
mainPanelOpen,
darkMode,
selectedThemeMode,
fontSize,
dyslexic,
currency,
playerName,
setSubPanelOpen,
setLanguagePanelOpen,
setConfigPanelOpen,
setMainPanelReduced,
setMainPanelOpen,
setDarkMode,
setSelectedThemeMode,
setFontSize,
setDyslexic,
setCurrency,
setPlayerName,
}}
>
{props.children}
</AppContext.Provider>
);
};
}

View File

@ -1,72 +1,137 @@
query getWebsiteInterface($language_code: String) {
websiteInterfaces(filters: { language: { code: { eq: $language_code } } }) {
websiteInterfaces(
filters: { ui_language: { code: { eq: $language_code } } }
) {
data {
attributes {
main_library
main_library_description
main_news
main_merch
main_gallery
main_archives
main_about_us
main_licensing
main_copyright
library
contents
wiki
chronicles
library_short_description
contents_short_description
wiki_short_description
chronicles_short_description
news
merch
gallery
archives
about_us
licensing_notice
copyright_notice
contents_description
type
category
categories
size
release_date
release_year
details
price
width
height
thickness
subitem
subitems
subitem_of
variant
variants
variant_of
summary
audio
video
textual
game
other
return_to
left_to_right
right_to_left
page
pages
page_order
binding
type_information
front_matter
back_matter
open_content
read_content
watch_content
listen_content
view_scans
paperback
hardcover
languages
select_language
language
library_description
library_item_summary
library_item_gallery
library_item_details
library_item_subitems
library_item_variants
library_item_content
global_return_label
global_subitem_of
global_type
global_width
global_height
global_thickness
global_binding
global_language
global_languages
global_page
global_pages
global_page_order
global_release_date
global_price
library_item_physical_size
library_item_type_information
library_item_front_matter
library_item_back_matter
library_item_type_textual
library_item_type_audio
library_item_type_game
library_item_type_video
library_item_type_other
library_item_open_content
library_item_view_scans
content_read_content
content_watch_content
content_listen_content
global_category
global_categories
global_paperback
global_hardcover
global_left_to_right
global_right_to_left
main_wiki
main_wiki_description
main_chronicles
main_chronicles_description
library_items
library_items_description
library_content
library_content_description
wiki_description
news_description
chronicles_description
news_description
merch_description
gallery_description
archives_description
about_us_description
merch_description
page_not_found
default_description
name
show_subitems
show_primary_items
show_secondary_items
no_type
no_year
order_by
group_by
select_option_sidebar
group
settings
theme
light
auto
dark
font_size
player_name
currency
font
calculated
status_incomplete
status_draft
status_review
status_done
incomplete
draft
review
done
status
transcribers
translators
proofreaders
transcript_notice
translation_notice
source_language
pronouns
no_category
item
items
content
result
results
language_switch_message
open_settings
change_language
open_search
chronology
accords_handbook
legality
members
sharing_policy
contact_us
email
email_gdpr_notice
message
send
response_invalid_code
response_invalid_email
response_email_success
}
}
}
@ -125,17 +190,15 @@ query getChronologyItems($language_code: String) {
}
query getLibraryItemsPreview($language_code: String) {
libraryItems(
filters: { root_item: { eq: true } }
pagination: { limit: -1 }
sort: ["title:asc", "subtitle:asc"]
) {
libraryItems(pagination: { limit: -1 }) {
data {
id
attributes {
title
subtitle
slug
root_item
primary
thumbnail {
data {
attributes {
@ -160,10 +223,20 @@ query getLibraryItemsPreview($language_code: String) {
attributes {
symbol
code
rate_to_usd
}
}
}
}
categories {
data {
id
attributes {
name
short
}
}
}
metadata {
__typename
... on ComponentMetadataBooks {
@ -218,7 +291,7 @@ query getLibraryItemsPreview($language_code: String) {
}
}
}
... on ComponentMetadataOther {
... on ComponentMetadataGroup {
subtype {
data {
attributes {
@ -231,6 +304,18 @@ query getLibraryItemsPreview($language_code: String) {
}
}
}
subitems_type {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
}
}
@ -296,10 +381,20 @@ query getLibraryItem($slug: String, $language_code: String) {
attributes {
symbol
code
rate_to_usd
}
}
}
}
categories {
data {
id
attributes {
name
short
}
}
}
size {
width
height
@ -396,7 +491,7 @@ query getLibraryItem($slug: String, $language_code: String) {
}
}
}
... on ComponentMetadataOther {
... on ComponentMetadataGroup {
subtype {
data {
attributes {
@ -409,6 +504,18 @@ query getLibraryItem($slug: String, $language_code: String) {
}
}
}
subitems_type {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
}
subitem_of {
@ -452,10 +559,20 @@ query getLibraryItem($slug: String, $language_code: String) {
attributes {
symbol
code
rate_to_usd
}
}
}
}
categories {
data {
id
attributes {
name
short
}
}
}
metadata {
__typename
... on ComponentMetadataBooks {
@ -516,7 +633,7 @@ query getLibraryItem($slug: String, $language_code: String) {
}
}
}
... on ComponentMetadataOther {
... on ComponentMetadataGroup {
subtype {
data {
attributes {
@ -531,6 +648,20 @@ query getLibraryItem($slug: String, $language_code: String) {
}
}
}
subitems_type {
data {
attributes {
slug
titles(
filters: {
language: { code: { eq: $language_code } }
}
) {
title
}
}
}
}
}
}
}
@ -585,6 +716,7 @@ query getLibraryItem($slug: String, $language_code: String) {
data {
id
attributes {
name
short
}
}
@ -655,6 +787,7 @@ query getContents($language_code: String) {
data {
id
attributes {
name
short
}
}
@ -878,6 +1011,15 @@ query getContentText($slug: String, $language_code: String) {
}
}
}
text_set_languages: text_set {
language {
data {
attributes {
code
}
}
}
}
text_set(filters: { language: { code: { eq: $language_code } } }) {
status
text
@ -1005,3 +1147,417 @@ query getContentText($slug: String, $language_code: String) {
}
}
}
query getCurrencies {
currencies {
data {
id
attributes {
code
symbol
rate_to_usd
display_decimals
}
}
}
}
query getLanguages {
languages {
data {
id
attributes {
name
code
localized_name
}
}
}
}
query getPost($slug: String, $language_code: String) {
posts(filters: { slug: { eq: $slug } }) {
data {
id
attributes {
slug
updatedAt
date {
year
month
day
}
authors {
data {
id
attributes {
username
anonymize
anonymous_code
pronouns
bio(filters: { language: { code: { eq: $language_code } } }) {
bio
}
languages {
data {
attributes {
code
}
}
}
avatar {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
categories {
data {
id
attributes {
name
short
}
}
}
hidden
thumbnail {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
translations_languages: translations {
language {
data {
attributes {
code
}
}
}
}
translations(filters: { language: { code: { eq: $language_code } } }) {
status
title
excerpt
thumbnail {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
body
}
}
}
}
}
query getPostsSlugs {
posts(filters: { hidden: { eq: false } }) {
data {
id
attributes {
slug
}
}
}
}
query getPostsPreview($language_code: String) {
posts(filters: { hidden: { eq: false } }) {
data {
id
attributes {
slug
date {
year
month
day
}
categories {
data {
id
attributes {
short
}
}
}
thumbnail {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
translations(filters: { language: { code: { eq: $language_code } } }) {
title
excerpt
thumbnail {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
}
}
query getContentLanguages($slug: String) {
contents(filters: { slug: { eq: $slug } }) {
data {
attributes {
text_set {
language {
data {
attributes {
code
}
}
}
}
video_set {
language {
data {
attributes {
code
}
}
}
}
audio_set {
language {
data {
attributes {
code
}
}
}
}
}
}
}
}
query getLibraryItemScans($slug: String, $language_code: String) {
libraryItems(filters: { slug: { eq: $slug } }) {
data {
id
attributes {
slug
title
subtitle
thumbnail {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
contents(pagination: { limit: -1 }) {
data {
id
attributes {
slug
range {
__typename
... on ComponentRangePageRange {
starting_page
ending_page
}
... on ComponentRangeTimeRange {
starting_time
ending_time
}
}
scan_set_languages: scan_set {
language {
data {
attributes {
code
}
}
}
}
scan_set(
filters: {
or: [
{ language: { code: { eq: "xx" } } }
{ language: { code: { eq: $language_code } } }
]
}
) {
status
source_language {
data {
attributes {
code
}
}
}
scanners {
data {
id
attributes {
username
anonymize
anonymous_code
pronouns
bio(
filters: { language: { code: { eq: $language_code } } }
) {
bio
}
languages {
data {
attributes {
code
}
}
}
avatar {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
cleaners {
data {
id
attributes {
username
anonymize
anonymous_code
pronouns
bio(
filters: { language: { code: { eq: $language_code } } }
) {
bio
}
languages {
data {
attributes {
code
}
}
}
avatar {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
typesetters {
data {
id
attributes {
username
anonymize
anonymous_code
pronouns
bio(
filters: { language: { code: { eq: $language_code } } }
) {
bio
}
languages {
data {
attributes {
code
}
}
}
avatar {
data {
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
notes
pages(pagination: { limit: -1 }) {
data {
id
attributes {
name
alternativeText
caption
width
height
url
}
}
}
}
}
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
import { readFileSync } from "fs";
import {
GetChronologyItemsQuery,
GetChronologyItemsQueryVariables,
@ -11,19 +10,31 @@ import {
GetContentsSlugsQueryVariables,
GetContentTextQuery,
GetContentTextQueryVariables,
GetCurrenciesQuery,
GetCurrenciesQueryVariables,
GetErasQuery,
GetErasQueryVariables,
GetLanguagesQuery,
GetLanguagesQueryVariables,
GetLibraryItemQuery,
GetLibraryItemQueryVariables,
GetLibraryItemScansQuery,
GetLibraryItemScansQueryVariables,
GetLibraryItemsPreviewQuery,
GetLibraryItemsPreviewQueryVariables,
GetLibraryItemsSlugsQuery,
GetLibraryItemsSlugsQueryVariables,
GetPostQuery,
GetPostQueryVariables,
GetPostsPreviewQuery,
GetPostsPreviewQueryVariables,
GetPostsSlugsQuery,
GetPostsSlugsQueryVariables,
GetWebsiteInterfaceQuery,
GetWebsiteInterfaceQueryVariables,
} from "graphql/operations-types";
const graphQL = async (query: string, variables?: string) => {
async function graphQL(query: string, variables?: string) {
const res = await fetch(`${process.env.URL_GRAPHQL}`, {
method: "POST",
body: JSON.stringify({
@ -32,11 +43,11 @@ const graphQL = async (query: string, variables?: string) => {
}),
headers: {
"content-type": "application/json",
Authorization: "Bearer " + process.env.ACCESS_TOKEN,
Authorization: `Bearer ${process.env.ACCESS_TOKEN}`,
},
});
return (await res.json()).data;
};
}
function getQueryFromOperations(queryName: string): string {
const operations = readFileSync("./src/graphql/operation.graphql", "utf8");
@ -103,7 +114,6 @@ export async function getContentsSlugs(
return await graphQL(query, JSON.stringify(variables));
}
export async function getContents(
variables: GetContentsQueryVariables
): Promise<GetContentsQuery> {
@ -124,3 +134,45 @@ export async function getContentText(
const query = getQueryFromOperations("getContentText");
return await graphQL(query, JSON.stringify(variables));
}
export async function getCurrencies(
variables: GetCurrenciesQueryVariables
): Promise<GetCurrenciesQuery> {
const query = getQueryFromOperations("getCurrencies");
return await graphQL(query, JSON.stringify(variables));
}
export async function getLanguages(
variables: GetLanguagesQueryVariables
): Promise<GetLanguagesQuery> {
const query = getQueryFromOperations("getLanguages");
return await graphQL(query, JSON.stringify(variables));
}
export async function getPost(
variables: GetPostQueryVariables
): Promise<GetPostQuery> {
const query = getQueryFromOperations("getPost");
return await graphQL(query, JSON.stringify(variables));
}
export async function getPostsSlugs(
variables: GetPostsSlugsQueryVariables
): Promise<GetPostsSlugsQuery> {
const query = getQueryFromOperations("getPostsSlugs");
return await graphQL(query, JSON.stringify(variables));
}
export async function getPostsPreview(
variables: GetPostsPreviewQueryVariables
): Promise<GetPostsPreviewQuery> {
const query = getQueryFromOperations("getPostsPreview");
return await graphQL(query, JSON.stringify(variables));
}
export async function getLibraryItemScans(
variables: GetLibraryItemScansQueryVariables
): Promise<GetLibraryItemScansQuery> {
const query = getQueryFromOperations("getLibraryItemScans");
return await graphQL(query, JSON.stringify(variables));
}

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ export default function useDarkMode(
key: string,
initialValue: boolean | undefined
): [
boolean | undefined,
boolean | undefined,
React.Dispatch<React.SetStateAction<boolean | undefined>>,
React.Dispatch<React.SetStateAction<boolean | undefined>>
@ -23,5 +24,5 @@ export default function useDarkMode(
if (selectedThemeMode === false) setDarkMode(prefersDarkMode);
}, [selectedThemeMode, prefersDarkMode, setDarkMode]);
return [darkMode, setDarkMode, setSelectedThemeMode];
return [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode];
}

View File

@ -1,13 +1,13 @@
import { useEffect, useState } from "react";
export default function useMediaQuery(query: string): boolean {
const getMatches = (query: string): boolean => {
function getMatches(query: string): boolean {
// Prevents SSR issues
if (typeof window !== "undefined") {
return window.matchMedia(query).matches;
}
return false;
};
}
const [matches, setMatches] = useState<boolean>(getMatches(query));

View File

@ -1,37 +1,36 @@
import Link from "next/link";
import ContentPanel from "components/Panels/ContentPanel";
import { getWebsiteInterface } from "graphql/operations";
import { GetStaticProps } from "next";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import AppLayout from "components/AppLayout";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type FourOhFourProps = {
langui: GetWebsiteInterfaceQuery;
};
interface FourOhFourProps extends AppStaticProps {}
export default function FourOhFour(props: FourOhFourProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const contentPanel = (
<ContentPanel>
<h1>404 - Page Not Found</h1>
<Link href="/">
<a>Go back home</a>
</Link>
<h1>404 - {langui.page_not_found}</h1>
<ReturnButton
href="/"
title="Home"
langui={langui}
displayOn={ReturnButtonType.both}
/>
</ContentPanel>
);
return <AppLayout navTitle="404" langui={langui} contentPanel={contentPanel} />;
return <AppLayout navTitle="404" contentPanel={contentPanel} {...props} />;
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: FourOhFourProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: FourOhFourProps }> {
const props: FourOhFourProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -1,15 +1,17 @@
import "@fontsource/material-icons";
import "@fontsource/opendyslexic/400.css";
import "@fontsource/opendyslexic/700.css";
import "@fontsource/vollkorn/700.css";
import "@fontsource/zen-maru-gothic/500.css";
import "@fontsource/zen-maru-gothic/900.css";
import { AppContextProvider } from "contexts/AppLayoutContext";
import type { AppProps } from "next/app";
import "tailwind.css";
import "@fontsource/zen-maru-gothic/500.css";
import "@fontsource/vollkorn/700.css";
import "@fontsource/material-icons";
import { AppContextProvider } from "contexts/AppLayoutContext";
export default function AccordsLibraryApp(appProps: AppProps) {
export default function AccordsLibraryApp(props: AppProps): JSX.Element {
return (
<AppContextProvider>
<appProps.Component {...appProps.pageProps} />
<props.Component {...props.pageProps} />
</AppContextProvider>
);
}

View File

@ -1,18 +1,19 @@
import Document, {
Html,
DocumentContext,
Head,
Html,
Main,
NextScript,
DocumentContext,
} from "next/document";
class MyDocument extends Document {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
render(): JSX.Element {
return (
<Html>
<Head>

View File

@ -0,0 +1,97 @@
import AppLayout from "components/AppLayout";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { getPost } from "graphql/operations";
import { GetPostQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface AccordsHandbookProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
}
export default function AccordsHandbook(
props: AccordsHandbookProps
): JSX.Element {
const { langui, post } = props;
const router = useRouter();
const locales = getLocalesFromLanguages(post.translations_languages);
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
{post.translations.length > 0 && post.translations[0].body && (
<TOC
text={post.translations[0].body}
title={post.translations[0].title}
/>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return (
<AppLayout
navTitle={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: AccordsHandbookProps }> {
const slug = "accords-handbook";
const props: AccordsHandbookProps = {
...(await getAppStaticProps(context)),
post: (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -0,0 +1,241 @@
import AppLayout from "components/AppLayout";
import InsetBox from "components/InsetBox";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { getPost } from "graphql/operations";
import { GetPostQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, randomInt } from "queries/helpers";
import { useState } from "react";
interface ContactProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
}
export default function AboutUs(props: ContactProps): JSX.Element {
const { langui, post } = props;
const router = useRouter();
const [formResponse, setFormResponse] = useState("");
const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">(
"stale"
);
const locales = getLocalesFromLanguages(post.translations_languages);
const [randomNumber1, setRandomNumber1] = useState(randomInt(0, 10));
const [randomNumber2, setRandomNumber2] = useState(randomInt(0, 10));
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
{post.translations.length > 0 && post.translations[0].body && (
<TOC
text={post.translations[0].body}
title={post.translations[0].title}
/>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
<div className="flex flex-col gap-8 text-center">
<form
className={`gap-8 grid ${
formState !== "stale" &&
"opacity-60 cursor-not-allowed touch-none pointer-events-none"
}`}
onSubmit={(event) => {
event.preventDefault();
const fields = event.target as unknown as {
verif: HTMLInputElement;
name: HTMLInputElement;
email: HTMLInputElement;
message: HTMLInputElement;
};
setFormState("ongoing");
if (
parseInt(fields.verif.value, 10) ===
randomNumber1 + randomNumber2 &&
formState !== "completed"
) {
const content: RequestMailProps = {
name: fields.name.value,
email: fields.email.value,
message: fields.message.value,
formName: "Contact Form",
};
fetch("/api/mail", {
method: "POST",
body: JSON.stringify(content),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then(async (responseJson) => responseJson.json())
.then((response: ResponseMailProps) => {
switch (response.code) {
case "OKAY":
setFormResponse(langui.response_email_success);
setFormState("completed");
break;
case "EENVELOPE":
setFormResponse(langui.response_invalid_email);
setFormState("stale");
break;
default:
setFormResponse(response.message ?? "");
setFormState("stale");
break;
}
});
} else {
setFormResponse(langui.response_invalid_code);
setFormState("stale");
setRandomNumber1(randomInt(0, 10));
setRandomNumber2(randomInt(0, 10));
}
router.replace("#send-response");
fields.verif.value = "";
}}
>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="name">{langui.name}:</label>
<input
type="text"
className="mobile:w-full"
name="name"
id="name"
required
disabled={formState !== "stale"}
/>
</div>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="email">{langui.email}:</label>
<input
type="email"
className="mobile:w-full"
name="email"
id="email"
required
disabled={formState !== "stale"}
/>
<p className="text-sm text-dark italic opacity-70">
{langui.email_gdpr_notice}
</p>
</div>
<div className="flex flex-col place-items-center gap-1 w-full">
<label htmlFor="message">{langui.message}:</label>
<textarea
name="message"
id="message"
className="w-full"
rows={8}
required
disabled={formState !== "stale"}
/>
</div>
<div className="grid grid-cols-2 place-items-center">
<div className="flex flex-row place-items-center gap-2">
<label
className="flex-shrink-0"
htmlFor="verif"
>{`${randomNumber1} + ${randomNumber2} =`}</label>
<input
className="w-24"
type="number"
name="verif"
id="verif"
required
disabled={formState !== "stale"}
/>
</div>
<input
type="submit"
value={langui.send}
className="w-min !px-6"
disabled={formState !== "stale"}
/>
</div>
</form>
<div id="send-response">
{formResponse && (
<InsetBox>
<p>{formResponse}</p>
</InsetBox>
)}
</div>
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={"Contact"}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: ContactProps }> {
const slug = "contact";
const props: ContactProps = {
...(await getAppStaticProps(context)),
post: (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,44 +1,48 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import AppLayout from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type AboutUsProps = {
langui: GetWebsiteInterfaceQuery;
};
interface AboutUsProps extends AppStaticProps {}
export default function AboutUs(props: AboutUsProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const subPanel = (
<SubPanel>
<PanelHeader
icon="info"
title={langui.main_about_us}
title={langui.about_us}
description={langui.about_us_description}
/>
<NavOption
title={langui.accords_handbook}
url="/about-us/accords-handbook"
border
/>
<NavOption title={langui.legality} url="/about-us/legality" border />
{/* <NavOption title={langui.members} url="/about-us/members" border /> */}
<NavOption
title={langui.sharing_policy}
url="/about-us/sharing-policy"
border
/>
<NavOption title={langui.contact_us} url="/about-us/contact" border />
</SubPanel>
);
return (
<AppLayout
navTitle={langui.main_about_us}
langui={langui}
subPanel={subPanel}
/>
<AppLayout navTitle={langui.about_us} subPanel={subPanel} {...props} />
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: AboutUsProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: AboutUsProps }> {
const props: AboutUsProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -0,0 +1,95 @@
import AppLayout from "components/AppLayout";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { getPost } from "graphql/operations";
import { GetPostQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface SiteInfoProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
}
export default function SiteInformation(props: SiteInfoProps): JSX.Element {
const { langui, post } = props;
const router = useRouter();
const locales = getLocalesFromLanguages(post.translations_languages);
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
{post.translations.length > 0 && post.translations[0].body && (
<TOC
text={post.translations[0].body}
title={post.translations[0].title}
/>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return (
<AppLayout
navTitle={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: SiteInfoProps }> {
const slug = "legality";
const props: SiteInfoProps = {
...(await getAppStaticProps(context)),
post: (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -0,0 +1,95 @@
import AppLayout from "components/AppLayout";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { getPost } from "graphql/operations";
import { GetPostQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface SharingPolicyProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
}
export default function SharingPolicy(props: SharingPolicyProps): JSX.Element {
const { langui, post } = props;
const locales = getLocalesFromLanguages(post.translations_languages);
const router = useRouter();
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
{post.translations.length > 0 && post.translations[0].body && (
<TOC
text={post.translations[0].body}
title={post.translations[0].title}
/>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return (
<AppLayout
navTitle={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: SharingPolicyProps }> {
const slug = "sharing-policy";
const props: SharingPolicyProps = {
...(await getAppStaticProps(context)),
post: (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0].attributes,
};
return {
props: props,
};
}

51
src/pages/api/mail.ts Normal file
View File

@ -0,0 +1,51 @@
import type { NextApiRequest, NextApiResponse } from "next";
import nodemailer from "nodemailer";
import { SMTPError } from "nodemailer/lib/smtp-connection";
export type ResponseMailProps = {
code?: string;
message?: string;
};
export type RequestMailProps = {
name: string;
email: string;
message: string;
formName: string;
};
export default async function Mail(
req: NextApiRequest,
res: NextApiResponse<ResponseMailProps>
) {
if (req.method === "POST") {
const body = req.body as RequestMailProps;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
// send mail with defined transport object
await transporter
.sendMail({
from: `"${body.name}" <${body.email}>`,
to: "contact@accords-library.com",
subject: `New ${body.formName} from ${body.name}`,
text: body.message,
})
.catch((reason: SMTPError) => {
res.status(reason.responseCode ?? 500).json({
code: reason.code,
message: reason.response,
});
});
}
res.status(200).json({ code: "OKAY" });
}

View File

@ -1,44 +1,34 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import AppLayout from "components/AppLayout";
import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type ArchivesProps = {
langui: GetWebsiteInterfaceQuery;
};
interface ArchivesProps extends AppStaticProps {}
export default function Archives(props: ArchivesProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const subPanel = (
<SubPanel>
<PanelHeader
icon="inventory"
title={langui.main_archives}
title={langui.archives}
description={langui.archives_description}
/>
</SubPanel>
);
return (
<AppLayout
navTitle={langui.main_archives}
langui={langui}
subPanel={subPanel}
/>
<AppLayout navTitle={langui.archives} subPanel={subPanel} {...props} />
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: ArchivesProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: ArchivesProps }> {
const props: ArchivesProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -1,44 +1,34 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import AppLayout from "components/AppLayout";
import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type ChroniclesProps = {
langui: GetWebsiteInterfaceQuery;
};
interface ChroniclesProps extends AppStaticProps {}
export default function Chronicles(props: ChroniclesProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const subPanel = (
<SubPanel>
<PanelHeader
icon="watch_later"
title={langui.main_chronicles}
title={langui.chronicles}
description={langui.chronicles_description}
/>
</SubPanel>
);
return (
<AppLayout
navTitle={langui.main_chronicles}
langui={langui}
subPanel={subPanel}
/>
<AppLayout navTitle={langui.chronicles} subPanel={subPanel} {...props} />
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: ChroniclesProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(context: GetStaticPropsContext): Promise<{
props: ChroniclesProps;
}> {
const props: ChroniclesProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -1,70 +1,118 @@
import { GetStaticPaths, GetStaticProps } from "next";
import {
getContent,
getContentsSlugs,
getWebsiteInterface,
} from "graphql/operations";
import {
GetContentQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import ContentPanel from "components/Panels/ContentPanel";
import Button from "components/Button";
import HorizontalLine from "components/HorizontalLine";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import AppLayout from "components/AppLayout";
import Button from "components/Button";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import HorizontalLine from "components/HorizontalLine";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import ReturnButton from "components/PanelComponents/ReturnButton";
import { getContent, getContentsSlugs } from "graphql/operations";
import { GetContentQuery } from "graphql/operations-types";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyinlineTitle, prettySlug } from "queries/helpers";
type ContentIndexProps = {
content: GetContentQuery;
langui: GetWebsiteInterfaceQuery;
};
interface ContentIndexProps extends AppStaticProps {
content: GetContentQuery["contents"]["data"][number]["attributes"];
}
export default function ContentIndex(props: ContentIndexProps): JSX.Element {
const content = props.content.contents.data[0].attributes;
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { content, langui } = props;
const subPanel = (
<SubPanel>
<ReturnButton href="/contents" title={"Contents"} langui={langui} />
<HorizontalLine />
<ReturnButton
href="/contents"
title={"Contents"}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/contents"
title={"Contents"}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<div className="grid place-items-center">
<ThumbnailHeader content={content} langui={langui} />
<ThumbnailHeader
thumbnail={content.thumbnail.data?.attributes}
pre_title={
content.titles.length > 0 ? content.titles[0].pre_title : undefined
}
title={
content.titles.length > 0
? content.titles[0].title
: prettySlug(content.slug)
}
subtitle={
content.titles.length > 0 ? content.titles[0].subtitle : undefined
}
description={
content.titles.length > 0
? content.titles[0].description
: undefined
}
type={content.type}
categories={content.categories}
langui={langui}
/>
<HorizontalLine />
{content.text_set.length > 0 ? (
{content.text_set.length > 0 && (
<Button href={`/contents/${content.slug}/read/`}>
{langui.content_read_content}
{langui.read_content}
</Button>
) : (
""
)}
{content.audio_set.length > 0 ? (
{content.audio_set.length > 0 && (
<Button href={`/contents/${content.slug}/listen/`}>
{langui.content_listen_content}
{langui.listen_content}
</Button>
) : (
""
)}
{content.video_set.length > 0 ? (
{content.video_set.length > 0 && (
<Button href={`/contents/${content.slug}/watch/`}>
{langui.content_watch_content}
{langui.watch_content}
</Button>
) : (
""
)}
</div>
</ContentPanel>
);
let description = "";
if (content.type.data) {
description += `${langui.type}: `;
if (content.type.data.attributes.titles.length > 0) {
description += content.type.data.attributes.titles[0].title;
} else {
description += prettySlug(content.type.data.attributes.slug);
}
description += "\n";
}
if (content.categories.data.length > 0) {
description += `${langui.categories}: `;
description += content.categories.data
.map((category) => category.attributes.short)
.join(" | ");
description += "\n";
}
if (content.titles.length > 0 && content.titles[0].description) {
description += "\n";
description += content.titles[0].description;
}
return (
<AppLayout
navTitle="Contents"
@ -78,50 +126,37 @@ export default function ContentIndex(props: ContentIndexProps): JSX.Element {
: prettySlug(content.slug)
}
thumbnail={content.thumbnail.data?.attributes}
langui={langui}
contentPanel={contentPanel}
subPanel={subPanel}
description={
content.titles.length > 0 ? content.titles[0].description : undefined
}
description={description}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
if (context.params.slug && context.locale) {
if (context.params.slug instanceof Array)
context.params.slug = context.params.slug.join("");
const props: ContentIndexProps = {
content: await getContent({
slug: context.params.slug,
language_code: context.locale,
}),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
}
return { props: {} };
};
export const getStaticPaths: GetStaticPaths = async (context) => {
type Path = {
params: {
slug: string;
};
locale: string;
export async function getStaticProps(context: GetStaticPropsContext): Promise<{
props: ContentIndexProps;
}> {
const props: ContentIndexProps = {
...(await getAppStaticProps(context)),
content: (
await getContent({
slug: context.params?.slug?.toString() ?? "",
language_code: context.locale ?? "en",
})
).contents.data[0].attributes,
};
return {
props: props,
};
}
const data = await getContentsSlugs({});
const paths: Path[] = [];
data.contents.data.map((item) => {
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const contents = await getContentsSlugs({});
const paths: GetStaticPathsResult["paths"] = [];
contents.contents.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
@ -130,4 +165,4 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
paths,
fallback: false,
};
};
}

View File

@ -1,44 +1,47 @@
import { GetStaticPaths, GetStaticProps } from "next";
import {
getContentsSlugs,
getContentText,
getWebsiteInterface,
} from "graphql/operations";
import {
Enum_Componentsetstextset_Status,
GetContentTextQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import ContentPanel from "components/Panels/ContentPanel";
import HorizontalLine from "components/HorizontalLine";
import SubPanel from "components/Panels/SubPanel";
import ReturnButton from "components/PanelComponents/ReturnButton";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import AppLayout from "components/AppLayout";
import Button from "components/Button";
import Chip from "components/Chip";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import HorizontalLine from "components/HorizontalLine";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { getContentsSlugs, getContentText } from "graphql/operations";
import { GetContentTextQuery } from "graphql/operations-types";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
getLocalesFromLanguages,
getStatusDescription,
prettyinlineTitle,
prettyLanguage,
prettySlug,
prettyTestError,
prettyTestWarning,
} from "queries/helpers";
import Button from "components/Button";
import { useRouter } from "next/router";
import Chip from "components/Chip";
import ReactTooltip from "react-tooltip";
import RecorderChip from "components/RecorderChip";
interface ContentReadProps {
content: GetContentTextQuery;
langui: GetWebsiteInterfaceQuery;
interface ContentReadProps extends AppStaticProps {
content: GetContentTextQuery["contents"]["data"][number]["attributes"];
contentId: GetContentTextQuery["contents"]["data"][number]["id"];
}
export default function ContentRead(props: ContentReadProps): JSX.Element {
useTesting(props);
const content = props.content.contents.data[0].attributes;
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui, content, languages } = props;
const router = useRouter();
const locales = getLocalesFromLanguages(content.text_set_languages);
const subPanel = (
<SubPanel>
@ -46,23 +49,23 @@ export default function ContentRead(props: ContentReadProps): JSX.Element {
href={`/contents/${content.slug}`}
title={"Content"}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
<HorizontalLine />
{content.text_set.length > 0 ? (
{content.text_set.length > 0 && content.text_set[0].source_language.data && (
<div className="grid gap-5">
<h2 className="text-xl">
{content.text_set[0].source_language.data.attributes.code ===
router.locale
? "This content is a transcript"
: "This content is a fan-translation"}
? langui.transcript_notice
: langui.translation_notice}
</h2>
{content.text_set[0].source_language.data.attributes.code !==
router.locale && (
<div className="grid place-items-center gap-2">
<p className="font-headers">Source language:</p>
<p className="font-headers">{langui.source_language}:</p>
<Button
href={router.asPath}
locale={
@ -70,40 +73,34 @@ export default function ContentRead(props: ContentReadProps): JSX.Element {
}
>
{prettyLanguage(
content.text_set[0].source_language.data.attributes.code
content.text_set[0].source_language.data.attributes.code,
languages
)}
</Button>
</div>
)}
<div className="grid grid-flow-col place-items-center place-content-center gap-2">
<p className="font-headers">Status:</p>
<p className="font-headers">{langui.status}:</p>
<Chip
data-tip={
content.text_set[0].status ===
Enum_Componentsetstextset_Status.Incomplete
? "This entry is only partially translated/transcribed."
: content.text_set[0].status ===
Enum_Componentsetstextset_Status.Draft
? "This entry is just a draft. It usually means that this is a work-in-progress. Translation/transcription might be poor and/or computer-generated."
: content.text_set[0].status ===
Enum_Componentsetstextset_Status.Review
? "This entry has not yet being proofread. The content should still be accurate."
: "This entry has been checked and proofread. If you notice any translation errors or typos, please contact us so we can fix it!"
}
data-for={"StatusTooltip"}
<ToolTip
content={getStatusDescription(content.text_set[0].status, langui)}
maxWidth={"20rem"}
>
{content.text_set[0].status}
</Chip>
<Chip>{content.text_set[0].status}</Chip>
</ToolTip>
</div>
{content.text_set[0].transcribers.data.length > 0 && (
<div>
<p className="font-headers">Transcribers:</p>
<p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].transcribers.data.map((recorder) => (
<RecorderChip key={recorder.id} recorder={recorder} />
<RecorderChip
key={recorder.id}
langui={langui}
recorder={recorder}
/>
))}
</div>
</div>
@ -111,10 +108,14 @@ export default function ContentRead(props: ContentReadProps): JSX.Element {
{content.text_set[0].translators.data.length > 0 && (
<div>
<p className="font-headers">Translators:</p>
<p className="font-headers">{langui.translators}:</p>
<div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].translators.data.map((recorder) => (
<RecorderChip key={recorder.id} recorder={recorder} />
<RecorderChip
key={recorder.id}
langui={langui}
recorder={recorder}
/>
))}
</div>
</div>
@ -122,61 +123,105 @@ export default function ContentRead(props: ContentReadProps): JSX.Element {
{content.text_set[0].proofreaders.data.length > 0 && (
<div>
<p className="font-headers">Proofreaders:</p>
<p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].proofreaders.data.map((recorder) => (
<RecorderChip key={recorder.id} recorder={recorder} />
<RecorderChip
key={recorder.id}
langui={langui}
recorder={recorder}
/>
))}
</div>
</div>
)}
</div>
) : (
""
)}
{content.text_set.length > 0 && content.text_set[0].text && (
<>
<HorizontalLine />
<TOC
text={content.text_set[0].text}
title={
content.titles.length > 0
? prettyinlineTitle(
content.titles[0].pre_title,
content.titles[0].title,
content.titles[0].subtitle
)
: prettySlug(content.slug)
}
/>
</>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href={`/contents/${content.slug}`}
title={langui.content}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<div className="grid place-items-center">
<ThumbnailHeader content={content} langui={langui} />
<ThumbnailHeader
thumbnail={content.thumbnail.data?.attributes}
pre_title={
content.titles.length > 0 ? content.titles[0].pre_title : undefined
}
title={
content.titles.length > 0
? content.titles[0].title
: prettySlug(content.slug)
}
subtitle={
content.titles.length > 0 ? content.titles[0].subtitle : undefined
}
description={
content.titles.length > 0
? content.titles[0].description
: undefined
}
type={content.type}
categories={content.categories}
langui={langui}
/>
<HorizontalLine />
{content.text_set.length > 0 ? (
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={content.text_set[0].text} />
) : (
""
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</div>
</ContentPanel>
);
const extra = (
<>
<ReactTooltip
id="StatusTooltip"
place="top"
type="light"
effect="solid"
delayShow={50}
clickable={true}
className="drop-shadow-shade-xl !opacity-100 !bg-light !rounded-lg desktop:after:!border-t-light text-left !text-black max-w-xs"
/>
<ReactTooltip
id="RecordersTooltip"
place="top"
type="light"
effect="solid"
delayShow={100}
delayUpdate={100}
delayHide={100}
clickable={true}
className="drop-shadow-shade-xl !opacity-100 !bg-light !rounded-lg desktop:after:!border-t-light text-left !text-black max-w-[22rem]"
/>
</>
);
let description = "";
if (content.type.data) {
description += `${langui.type}: `;
if (content.type.data.attributes.titles.length > 0) {
description += content.type.data.attributes.titles[0].title;
} else {
description += prettySlug(content.type.data.attributes.slug);
}
description += "\n";
}
if (content.categories.data.length > 0) {
description += `${langui.categories}: `;
description += content.categories.data
.map((category) => category.attributes.short)
.join(" | ");
description += "\n";
}
return (
<AppLayout
@ -191,48 +236,40 @@ export default function ContentRead(props: ContentReadProps): JSX.Element {
: prettySlug(content.slug)
}
thumbnail={content.thumbnail.data?.attributes}
langui={langui}
contentPanel={contentPanel}
subPanel={subPanel}
extra={extra}
description={description}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
if (context.params.slug && context.locale) {
if (context.params.slug instanceof Array)
context.params.slug = context.params.slug.join("");
const props: ContentReadProps = {
content: await getContentText({
slug: context.params.slug,
language_code: context.locale,
}),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
}
return { props: {} };
};
export const getStaticPaths: GetStaticPaths = async (context) => {
type Path = {
params: {
slug: string;
};
locale: string;
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: ContentReadProps }> {
const slug = context.params?.slug?.toString() ?? "";
const content = (
await getContentText({
slug: slug,
language_code: context.locale ?? "en",
})
).contents.data[0];
const props: ContentReadProps = {
...(await getAppStaticProps(context)),
content: content.attributes,
contentId: content.id,
};
return {
props: props,
};
}
const data = await getContentsSlugs({});
const paths: Path[] = [];
data.contents.data.map((item) => {
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const contents = await getContentsSlugs({});
const paths: GetStaticPathsResult["paths"] = [];
contents.contents.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
@ -241,15 +278,13 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
paths,
fallback: false,
};
};
}
export function useTesting(props: ContentReadProps) {
function useTesting(props: ContentReadProps) {
const router = useRouter();
const content = props.content.contents.data[0].attributes;
const { content, contentId } = props;
const contentURL =
"/admin/content-manager/collectionType/api::content.content/" +
props.content.contents.data[0].id;
const contentURL = `/admin/content-manager/collectionType/api::content.content/${contentId}`;
if (router.locale === "en") {
if (content.categories.data.length === 0) {
@ -276,18 +311,17 @@ export function useTesting(props: ContentReadProps) {
}
if (content.text_set.length > 1) {
console.warn(
prettyTestError(
router,
"More than one textset for this language",
["content", "text_set"],
contentURL
)
prettyTestError(
router,
"More than one textset for this language",
["content", "text_set"],
contentURL
);
}
if (content.text_set.length === 1) {
const textset = content.text_set[0];
if (!textset.text) {
prettyTestError(
router,
@ -303,8 +337,7 @@ export function useTesting(props: ContentReadProps) {
["content", "text_set"],
contentURL
);
}
if (textset.source_language.data.attributes.code === router.locale) {
} else if (textset.source_language.data.attributes.code === router.locale) {
// This is a transcript
if (textset.transcribers.data.length === 0) {
prettyTestError(

View File

@ -1,27 +1,114 @@
import { GetStaticProps } from "next";
import SubPanel from "components/Panels/SubPanel";
import AppLayout from "components/AppLayout";
import Chip from "components/Chip";
import LibraryContentPreview from "components/Library/LibraryContentPreview";
import PanelHeader from "components/PanelComponents/PanelHeader";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import Select from "components/Select";
import { getContents } from "graphql/operations";
import {
GetContentsQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { getContents, getWebsiteInterface } from "graphql/operations";
import PanelHeader from "components/PanelComponents/PanelHeader";
import AppLayout from "components/AppLayout";
import LibraryContentPreview from "components/Library/LibraryContentPreview";
import { prettyinlineTitle } from "queries/helpers";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyinlineTitle, prettySlug } from "queries/helpers";
import { useEffect, useState } from "react";
type LibraryProps = {
contents: GetContentsQuery;
langui: GetWebsiteInterfaceQuery;
};
interface ContentsProps extends AppStaticProps {
contents: GetContentsQuery["contents"]["data"];
}
export default function Library(props: LibraryProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
type GroupContentItems = Map<string, GetContentsQuery["contents"]["data"]>;
props.contents.contents.data.sort((a, b) => {
export default function Contents(props: ContentsProps): JSX.Element {
const { langui, contents } = props;
const [groupingMethod, setGroupingMethod] = useState<number>(-1);
const [groups, setGroups] = useState<GroupContentItems>(
getGroups(langui, groupingMethod, contents)
);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, contents));
}, [langui, groupingMethod, contents]);
const subPanel = (
<SubPanel>
<PanelHeader
icon="workspaces"
title={langui.contents}
description={langui.contents_description}
/>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.group_by}:</p>
<Select
className="w-full"
options={[langui.category, langui.type]}
state={groupingMethod}
setState={setGroupingMethod}
allowEmpty
/>
</div>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
{[...groups].map(([name, items]) => (
<>
{items.length > 0 && (
<>
{name && (
<h2
key={`h2${name}`}
className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2"
>
{name}
<Chip>{`${items.length} ${
items.length <= 1
? langui.result.toLowerCase()
: langui.results.toLowerCase()
}`}</Chip>
</h2>
)}
<div
key={`items${name}`}
className="grid gap-8 items-end grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
>
{items.map((item) => (
<LibraryContentPreview key={item.id} item={item.attributes} />
))}
</div>
</>
)}
</>
))}
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.contents}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: ContentsProps }> {
const contents = (
await getContents({
language_code: context.locale ?? "en",
})
).contents.data;
contents.sort((a, b) => {
const titleA =
a.attributes.titles.length > 0
? prettyinlineTitle(
@ -41,48 +128,73 @@ export default function Library(props: LibraryProps): JSX.Element {
return titleA.localeCompare(titleB);
});
const subPanel = (
<SubPanel>
<PanelHeader
icon="workspaces"
title="Contents"
description="Laboriosam vitae velit quis. Non et dolor reiciendis officia earum et molestias excepturi. Cupiditate officiis quis qui reprehenderit. Ut neque eos ipsa corrupti autem mollitia inventore. Exercitationem iste magni vel harum."
/>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]">
{props.contents.contents.data.map((item) => (
<LibraryContentPreview key={item.id} item={item.attributes} />
))}
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle="Contents"
langui={langui}
subPanel={subPanel}
contentPanel={contentPanel}
/>
);
const props: ContentsProps = {
...(await getAppStaticProps(context)),
contents: contents,
};
return {
props: props,
};
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: LibraryProps = {
contents: await getContents({
language_code: context.locale,
}),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
} else {
return { props: {} };
function getGroups(
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"],
groupByType: number,
items: ContentsProps["contents"]
): GroupContentItems {
switch (groupByType) {
case 0: {
const group = new Map();
group.set("Drakengard 1", []);
group.set("Drakengard 1.3", []);
group.set("Drakengard 2", []);
group.set("Drakengard 3", []);
group.set("Drakengard 4", []);
group.set("NieR Gestalt", []);
group.set("NieR Replicant", []);
group.set("NieR Replicant ver.1.22474487139...", []);
group.set("NieR:Automata", []);
group.set("NieR Re[in]carnation", []);
group.set("SINoALICE", []);
group.set("Voice of Cards", []);
group.set("Final Fantasy XIV", []);
group.set("Thou Shalt Not Die", []);
group.set("Bakuken", []);
group.set("YoRHa", []);
group.set("YoRHa Boys", []);
group.set(langui.no_category, []);
items.map((item) => {
if (item.attributes.categories.data.length === 0) {
group.get(langui.no_category)?.push(item);
} else {
item.attributes.categories.data.map((category) => {
group.get(category.attributes.name)?.push(item);
});
}
});
return group;
}
case 1: {
const group: GroupContentItems = new Map();
items.map((item) => {
const type =
item.attributes.type.data.attributes.titles.length > 0
? item.attributes.type.data.attributes.titles[0].title
: prettySlug(item.attributes.type.data.attributes.slug);
if (!group.has(type)) group.set(type, []);
group.get(type)?.push(item);
});
return group;
}
default: {
const group: GroupContentItems = new Map();
group.set("", items);
return group;
}
}
};
}

View File

@ -1,23 +1,19 @@
import AppLayout from "components/AppLayout";
import Markdawn from "components/Markdown/Markdawn";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import { getWebsiteInterface } from "graphql/operations";
import { GetStaticProps } from "next";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import AppLayout from "components/AppLayout";
import { useCallback, useState } from "react";
import Markdawn from "components/Markdown/Markdawn";
import { GetStaticPropsContext } from "next";
import Script from "next/script";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { useCallback, useState } from "react";
import { default as TurndownService } from "turndown";
type EditorProps = {
langui: GetWebsiteInterfaceQuery;
};
interface EditorProps extends AppStaticProps {}
export default function Editor(props: EditorProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const handleInput = useCallback((e) => {
setMarkdown(e.target.value);
const handleInput = useCallback((event) => {
setMarkdown(event.target.value);
}, []);
const [markdown, setMarkdown] = useState("");
@ -45,14 +41,15 @@ export default function Editor(props: EditorProps): JSX.Element {
onInput={handleInput}
className="bg-mid rounded-xl p-8 w-full font-monospace"
value={markdown}
title="Input textarea"
/>
<h2 className="mt-4">Convert text to markdown</h2>
<textarea
readOnly
id="htmlMdTextArea"
title="Ouput textarea"
onPaste={(event) => {
const TurndownService = require("turndown").default;
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
@ -62,16 +59,16 @@ export default function Editor(props: EditorProps): JSX.Element {
});
let paste = event.clipboardData.getData("text/html");
paste = paste.replace(/<\!--.*?-->/g, "");
paste = paste.replace(/<!--.*?-->/u, "");
paste = turndownService.turndown(paste);
paste = paste.replace(/<\!--.*?-->/g, "");
paste = paste.replace(/<!--.*?-->/u, "");
const target = event.target as HTMLTextAreaElement;
target.value = paste;
target.select();
event.preventDefault();
}}
className="bg-mid rounded-xl p-8 w-full font-monospace"
className="font-monospace"
/>
</div>
<div>
@ -86,22 +83,19 @@ export default function Editor(props: EditorProps): JSX.Element {
return (
<AppLayout
navTitle="Markdawn Editor"
langui={langui}
contentPanel={contentPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: EditorProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: EditorProps }> {
const props: EditorProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -1,14 +1,11 @@
import AppLayout from "components/AppLayout";
import { getWebsiteInterface } from "graphql/operations";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type GalleryProps = {
langui: GetWebsiteInterfaceQuery;
};
interface GalleryProps extends AppStaticProps {}
export default function Gallery(props: GalleryProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const contentPanel = (
<iframe
className="w-full h-screen"
@ -18,23 +15,20 @@ export default function Gallery(props: GalleryProps): JSX.Element {
return (
<AppLayout
navTitle={langui.main_gallery}
langui={langui}
navTitle={langui.gallery}
contentPanel={contentPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: GalleryProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: GalleryProps }> {
const props: GalleryProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -1,167 +1,71 @@
import AppLayout from "components/AppLayout";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import ContentPanel from "components/Panels/ContentPanel";
import SVG from "components/SVG";
import { getWebsiteInterface } from "graphql/operations";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
type HomeProps = {
langui: GetWebsiteInterfaceQuery;
};
import { getPost } from "graphql/operations";
import { GetPostQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface HomeProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
}
export default function Home(props: HomeProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { post } = props;
const locales = getLocalesFromLanguages(post.translations_languages);
const router = useRouter();
const contentPanel = (
<ContentPanel autoformat>
<ContentPanel>
<div className="grid place-items-center place-content-center w-full gap-5 text-center">
<div className="[mask:url('/icons/accords.svg')] [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square mobile:w-[50vw] bg-black" />
<h1 className="text-5xl mb-0">Accord&rsquo;s Library</h1>
<h2 className="mt-0">Discover Analyse Translate Archive</h2>
<h2 className="text-xl -mt-5">
Discover Analyse Translate Archive
</h2>
</div>
<h2>What is this?</h2>
<p>
Accord&rsquo;s Library aims at gathering and archiving all of Yoko
Taro&rsquo;s work. Yoko Taro is a Japanese video game director and
scenario writer. He is best-known for his work on the NieR and
Drakengard (Drag-on Dragoon) franchises. To complement his games, Yoko
Taro likes to publish side materials in the form of books, novellas,
artbooks, stage plays, manga, drama CDs, and comics. Those side
materials can be very difficult to find. His work goes all the way back
to 2003, and most of them are out of print after having been released
solely in Japan, sometimes in limited quantities. Their prices on the
second hand market have skyrocketed, ranging all the way to hundreds if
not thousand of dollars for the rarest items.&nbsp;
</p>
<p>
This is where this library takes its meaning, in trying to help the
community grow by providing translators, writers, and wiki&rsquo;s
contributors a simple way to access these records filled with stories,
artworks, and knowledge.
</p>
<p>
We are a small group of Yoko Taro&rsquo;s fans that decided to join
forces and create a website and a community. Our motto is{" "}
<strong>Discover Analyze Translate Archive</strong> (D.A.T.A. for
short). We started with the goal of gathering and archiving as much
side-materials/merch as possible. But since then, our ambition grew and
we decided to create a full-fledged website that will also include news
articles, lore, summaries, translations, and transcriptions. Hopefully
one day, we will be up there in the list of notable resources for
Drakengard and NieR fans.
</p>
<h2>What&rsquo;s on this website?</h2>
<p>
<strong>
<a href="https://accords-library.com/compendium/">The Compendium</a>
</strong>
: This is where we will list every NieR/DOD/other Yoko Tato merch,
games, books, novel, stage play, CD... well everything! For each, we
will provide photos and/or scans of the content, information about what
it is, when and how it was released, size, initial price...
</p>
<p>
<strong>
<a href="https://accords-library.com/news/">News</a>
</strong>
: Yes because we also want to create our own content! So there you will
find translations, transcriptions, unboxing, news about future
merch/game releases, maybe some guides. We don&rsquo;t see this website
as being purely a showcase of our work, but also of the community, and
as such, we will be accepting applications for becoming contributors on
the website. For the applicant, there is no deadline or article quota,
it merely means that we will have access to the website Post Writing
tools and will be able to submit a draft that can be published once
verified by an editor. Anyway, that&rsquo;s at least the plan, we will
think more about this until the website&rsquo;s official launch.
</p>
<p>
<strong>
<a href="https://accords-library.com/data/">Data</a>
</strong>
: There we will publish lore/knowledge about the Yokoverse: Dictionary,
Timeline, Weapons Stories, Game summaries... We have not yet decided how
deep we want to go as they are already quite a few resources out there.{" "}
</p>
<p>
<strong>
<a
href="https://gallery.accords-library.com/posts"
target="_blank"
rel="noreferrer noopener"
>
Gallery
</a>
</strong>
: A fully tagged Danbooru-styled gallery with currently more than a
thousand unique artworks. If you are unfamiliar with this kind of
gallery, it comes with a powerful search function that allows you to
search for specific images: want to search for images with both Caim and
Inuart, just type{" "}
<kbd>
<a
href="https://gallery.accords-library.com/posts/query=Caim%20Inuart"
target="_blank"
rel="noreferrer noopener"
>
Caim Inuart
</a>
</kbd>
. If you want images of Devola OR Popola, you can use a comma{" "}
<kbd>
<a
href="https://gallery.accords-library.com/posts/query=Popola%2CDevola"
data-type="URL"
data-id="https://gallery.accords-library.com/posts/query=Popola%2CDevola"
target="_blank"
rel="noreferrer noopener"
>
Popola,Devola
</a>
</kbd>
. You can also negate a tag: i.e. images of 9S without any pods around,
search for{" "}
<kbd>
<a
href="https://gallery.accords-library.com/posts/query=9S%20-Pods"
target="_blank"
rel="noreferrer noopener"
>
9S -Pods
</a>
</kbd>
. Anyway, there is a lot more to it, you can click on &quot;Syntax
help&quot; next to the Search button for even neater functions. Btw, you
can create an account to favorite, upvote/downvote posts, or if you want
to help tagging them. There isn&rsquo;t currently a way for new users to
upload images, you&rsquo;ll have to contact us first and we can decide
to enable this function on your account.
</p>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return (
<>
<AppLayout
navTitle={"Accords Library"}
langui={langui}
contentPanel={contentPanel}
/>
</>
<AppLayout
navTitle={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
contentPanel={contentPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: HomeProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
} else {
return { props: {} };
}
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: HomeProps }> {
const slug = "home";
const props: HomeProps = {
...(await getAppStaticProps(context)),
post: (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0].attributes,
};
return {
props: props,
};
}

View File

@ -1,925 +0,0 @@
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import { GetStaticPaths, GetStaticProps } from "next";
import {
getLibraryItem,
getLibraryItemsSlugs,
getWebsiteInterface,
} from "graphql/operations";
import {
Enum_Componentmetadatabooks_Binding_Type,
Enum_Componentmetadatabooks_Page_Order,
GetLibraryItemQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import {
convertMmToInch,
prettyDate,
prettyinlineTitle,
prettyItemType,
prettyItemSubType,
prettyPrice,
prettySlug,
prettyTestError,
prettyTestWarning,
sortContent,
} from "queries/helpers";
import SubPanel from "components/Panels/SubPanel";
import ReturnButton from "components/PanelComponents/ReturnButton";
import NavOption from "components/PanelComponents/NavOption";
import Chip from "components/Chip";
import Button from "components/Button";
import HorizontalLine from "components/HorizontalLine";
import AppLayout from "components/AppLayout";
import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
import InsetBox from "components/InsetBox";
import Img, { ImageQuality } from "components/Img";
import { useAppLayout } from "contexts/AppLayoutContext";
import { useRouter } from "next/router";
interface LibrarySlugProps {
libraryItem: GetLibraryItemQuery;
langui: GetWebsiteInterfaceQuery;
}
export default function LibrarySlug(props: LibrarySlugProps): JSX.Element {
useTesting(props);
const item = props.libraryItem.libraryItems.data[0].attributes;
const langui = props.langui.websiteInterfaces.data[0].attributes;
const appLayout = useAppLayout();
const isVariantSet =
item.metadata.length > 0 &&
item.metadata[0].__typename === "ComponentMetadataOther" &&
item.metadata[0].subtype.data.attributes.slug === "variant-set";
sortContent(item.contents);
const subPanel = (
<SubPanel>
<ReturnButton
href="/library/"
title={langui.main_library}
langui={langui}
/>
<HorizontalLine />
<div className="grid gap-4">
<NavOption
title={langui.library_item_summary}
url="#summary"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
{item.gallery.data.length > 0 ? (
<NavOption
title={langui.library_item_gallery}
url="#gallery"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
) : (
""
)}
<NavOption
title={langui.library_item_details}
url="#details"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
{item.subitems.data.length > 0 ? (
<NavOption
title={
isVariantSet
? langui.library_item_variants
: langui.library_item_subitems
}
url={isVariantSet ? "#variants" : "#subitems"}
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
) : (
""
)}
{item.contents.data.length > 0 ? (
<NavOption
title={langui.library_item_content}
url="#content"
border
/>
) : (
""
)}
</div>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid place-items-center gap-12">
<div className="drop-shadow-shade-xl w-full h-[50vh] mobile:h-[80vh] mb-16 relative cursor-pointer">
{item.thumbnail.data ? (
<Img
image={item.thumbnail.data.attributes}
quality={ImageQuality.Medium}
layout="fill"
objectFit="contain"
priority
/>
) : (
<div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div>
)}
</div>
<InsetBox id="summary" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8">
{item.subitem_of.data.length > 0 ? (
<div className="grid place-items-center">
<p>{langui.global_subitem_of}</p>
<Button
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
>
{prettyinlineTitle(
"",
item.subitem_of.data[0].attributes.title,
item.subitem_of.data[0].attributes.subtitle
)}
</Button>
</div>
) : (
""
)}
<div className="grid place-items-center">
<h1 className="text-3xl">{item.title}</h1>
{item.subtitle ? (
<h2 className="text-2xl">{item.subtitle}</h2>
) : (
""
)}
</div>
{item.descriptions.length > 0 ? (
<p className="text-justify">{item.descriptions[0].description}</p>
) : (
""
)}
</div>
</InsetBox>
{item.gallery.data.length > 0 ? (
<div id="gallery" className="grid place-items-center gap-8 w-full">
<h2 className="text-2xl">{langui.library_item_gallery}</h2>
<div className="grid w-full gap-8 items-end grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]">
{item.gallery.data.map((galleryItem) => (
<div
key={galleryItem.id}
className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer"
>
<div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div>
<Img
className="rounded-lg"
image={galleryItem.attributes}
layout="fill"
objectFit="cover"
/>
</div>
))}
</div>
</div>
) : (
""
)}
<InsetBox id="details" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items gap-8">
<h2 className="text-2xl text-center">
{langui.library_item_details}
</h2>
<div className="grid grid-flow-col w-full place-content-between">
{item.metadata.length > 0 ? (
<div className="grid place-items-center">
<h3 className="text-xl">{langui.global_type}</h3>
<div className="grid grid-flow-col gap-1">
<Chip>{prettyItemType(item.metadata[0], langui)}</Chip>
{""}
<Chip>{prettyItemSubType(item.metadata[0])}</Chip>
</div>
</div>
) : (
""
)}
{item.release_date ? (
<div className="grid place-items-center">
<h3 className="text-xl">{langui.global_release_date}</h3>
<p>{prettyDate(item.release_date)}</p>
</div>
) : (
""
)}
{item.price ? (
<div className="grid place-items-center">
<h3 className="text-xl">{langui.global_price}</h3>
<p>{prettyPrice(item.price)}</p>
</div>
) : (
""
)}
</div>
{item.size ? (
<>
<h3 className="text-xl">{langui.library_item_physical_size}</h3>
<div className="grid grid-flow-col w-full place-content-between">
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.global_width}:</p>
<div>
<p>{item.size.width} mm</p>
<p>{convertMmToInch(item.size.width)} in</p>
</div>
</div>
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.global_height}:</p>
<div>
<p>{item.size.height} mm</p>
<p>{convertMmToInch(item.size.height)} in</p>
</div>
</div>
{item.size.thickness ? (
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.global_thickness}:</p>
<div>
<p>{item.size.thickness} mm</p>
<p>{convertMmToInch(item.size.thickness)} in</p>
</div>
</div>
) : (
""
)}
</div>
</>
) : (
""
)}
{item.metadata.length > 0 ? (
<>
<h3 className="text-xl">
{langui.library_item_type_information}
</h3>
<div className="grid grid-cols-2 w-full place-content-between">
{item.metadata[0].__typename === "ComponentMetadataBooks" ? (
<>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.global_pages}:</p>
<p>{item.metadata[0].page_count}</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.global_binding}:</p>
<p>
{item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Paperback
? langui.global_paperback
: item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Hardcover
? langui.global_hardcover
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.global_page_order}:</p>
<p>
{item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.LeftToRight
? langui.global_left_to_right
: item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.RightToLeft
? langui.global_right_to_left
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.global_languages}:</p>
{item.metadata[0].languages.data.map((lang) => (
<p key={lang.attributes.code}>
{lang.attributes.name}
</p>
))}
</div>
</>
) : item.metadata[0].__typename ===
"ComponentMetadataAudio" ? (
<></>
) : item.metadata[0].__typename ===
"ComponentMetadataVideo" ? (
<></>
) : item.metadata[0].__typename ===
"ComponentMetadataGame" ? (
<></>
) : item.metadata[0].__typename ===
"ComponentMetadataOther" ? (
<>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.global_type}:</p>
<Chip>
{item.metadata[0].subtype.data.attributes.titles
.length > 0
? item.metadata[0].subtype.data.attributes.titles[0]
.title
: prettySlug(
item.metadata[0].subtype.data.attributes.slug
)}
</Chip>
</div>
</>
) : (
""
)}
</div>
</>
) : (
""
)}
</div>
</InsetBox>
{item.subitems.data.length > 0 ? (
<div
id={isVariantSet ? "variants" : "subitems"}
className="grid place-items-center gap-8 w-full"
>
<h2 className="text-2xl">
{isVariantSet
? langui.library_item_variants
: langui.library_item_subitems}
</h2>
<div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full">
{item.subitems.data.map((subitem) => (
<LibraryItemsPreview
key={subitem.id}
item={subitem.attributes}
/>
))}
</div>
</div>
) : (
""
)}
{item.contents.data.length > 0 ? (
<div id="content" className="w-full grid place-items-center gap-8">
<h2 className="text-2xl">{langui.library_item_content}</h2>
<div className="grid gap-4 w-full">
{item.contents.data.map((content) => (
<div
id={content.attributes.slug}
key={content.id}
className="grid gap-2 px-4 rounded-lg target:bg-mid target:shadow-inner-sm target:shadow-shade target:h-auto target:py-3 target:my-2 target:[--displaySubContentMenu:grid] [--displaySubContentMenu:none]"
>
<div className="grid gap-4 place-items-center grid-cols-[auto_auto_1fr_auto_12ch] thin:grid-cols-[auto_auto_1fr_auto]">
<a href={`#${content.attributes.slug}`}>
<h3>
{content.attributes.content.data &&
content.attributes.content.data.attributes.titles
.length > 0
? prettyinlineTitle(
content.attributes.content.data.attributes
.titles[0].pre_title,
content.attributes.content.data.attributes
.titles[0].title,
content.attributes.content.data.attributes
.titles[0].subtitle
)
: prettySlug(content.attributes.slug, item.slug)}
</h3>
</a>
<div className="flex flex-row flex-wrap gap-1">
{content.attributes.content.data?.attributes.categories.data.map(
(category) => (
<Chip key={category.id}>
{category.attributes.short}
</Chip>
)
)}
</div>
<p className="border-b-2 h-4 w-full border-black border-dotted opacity-30"></p>
<p>
{content.attributes.range[0].__typename ===
"ComponentRangePageRange"
? content.attributes.range[0].starting_page
: ""}
</p>
{content.attributes.content.data ? (
<Chip className="justify-self-end thin:hidden">
{content.attributes.content.data.attributes.type.data
.attributes.titles.length > 0
? content.attributes.content.data.attributes.type.data
.attributes.titles[0].title
: prettySlug(
content.attributes.content.data.attributes.type
.data.attributes.slug
)}
</Chip>
) : (
""
)}
</div>
<div className="grid-flow-col place-content-start place-items-center gap-2 [display:var(--displaySubContentMenu)]">
<span className="material-icons text-dark">
subdirectory_arrow_right
</span>
{content.attributes.scan_set.length > 0 ? (
<Button
href={`/contents/${content.attributes.content.data.attributes.slug}/scans/`}
>
{langui.library_item_view_scans}
</Button>
) : (
""
)}
{content.attributes.content.data ? (
<Button
href={`/contents/${content.attributes.content.data.attributes.slug}`}
>
{langui.library_item_open_content}
</Button>
) : (
""
)}
{content.attributes.scan_set.length === 0 &&
!content.attributes.content.data
? "The content is not available"
: ""}
</div>
</div>
))}
</div>
</div>
) : (
""
)}
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.main_library}
title={prettyinlineTitle("", item.title, item.subtitle)}
langui={langui}
contentPanel={contentPanel}
subPanel={subPanel}
thumbnail={item.thumbnail.data?.attributes}
description={
item.descriptions.length > 0
? item.descriptions[0].description
: undefined
}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.params) {
if (context.params.slug && context.locale) {
if (context.params.slug instanceof Array)
context.params.slug = context.params.slug.join("");
const props: LibrarySlugProps = {
libraryItem: await getLibraryItem({
slug: context.params.slug,
language_code: context.locale,
}),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
}
return { props: {} };
};
export const getStaticPaths: GetStaticPaths = async (context) => {
type Path = {
params: {
slug: string;
};
locale: string;
};
const data = await getLibraryItemsSlugs({});
const paths: Path[] = [];
data.libraryItems.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
});
return {
paths,
fallback: false,
};
};
function useTesting(props: LibrarySlugProps) {
const libraryItem = props.libraryItem.libraryItems.data[0].attributes;
const router = useRouter();
const libraryItemURL =
"/admin/content-manager/collectionType/api::library-item.library-item/" +
props.libraryItem.libraryItems.data[0].id;
sortContent(libraryItem.contents);
if (router.locale === "en") {
if (!libraryItem.thumbnail.data) {
prettyTestError(
router,
"Missing thumbnail",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.metadata.length === 0) {
prettyTestError(
router,
"Missing metadata",
["libraryItem"],
libraryItemURL
);
} else {
if (
libraryItem.metadata[0].__typename === "ComponentMetadataOther" &&
(libraryItem.metadata[0].subtype.data.attributes.slug ===
"relation-set" ||
libraryItem.metadata[0].subtype.data.attributes.slug ===
"variant-set")
) {
// This is a group type item
if (libraryItem.price) {
prettyTestError(
router,
"Group-type items shouldn't have price",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.size) {
prettyTestError(
router,
"Group-type items shouldn't have size",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.release_date) {
prettyTestError(
router,
"Group-type items shouldn't have release_date",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.contents.data.length > 0) {
prettyTestError(
router,
"Group-type items shouldn't have contents",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.subitems.data.length === 0) {
prettyTestError(
router,
"Group-type items should have subitems",
["libraryItem"],
libraryItemURL
);
}
} else {
// This is a normal item
if (libraryItem.metadata[0].__typename === "ComponentMetadataOther") {
if (
libraryItem.metadata[0].subtype.data.attributes.slug ===
"audio-case"
) {
let hasAudioSubItem = false;
libraryItem.subitems.data.map((subitem) => {
if (
subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata[0].__typename ===
"ComponentMetadataAudio"
)
hasAudioSubItem = true;
});
if (!hasAudioSubItem) {
prettyTestError(
router,
"Audio-case item doesn't have an audio-typed subitem",
["libraryItem"],
libraryItemURL
);
}
} else if (
libraryItem.metadata[0].subtype.data.attributes.slug === "game-case"
) {
let hasGameSubItem = false;
libraryItem.subitems.data.map((subitem) => {
if (
subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata[0].__typename ===
"ComponentMetadataGame"
)
hasGameSubItem = true;
});
if (!hasGameSubItem) {
prettyTestError(
router,
"Game-case item doesn't have an Game-typed subitem",
["libraryItem"],
libraryItemURL
);
}
} else if (
libraryItem.metadata[0].subtype.data.attributes.slug ===
"video-case"
) {
let hasVideoSubItem = false;
libraryItem.subitems.data.map((subitem) => {
if (
subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata[0].__typename ===
"ComponentMetadataVideo"
)
hasVideoSubItem = true;
});
if (!hasVideoSubItem) {
prettyTestError(
router,
"Video-case item doesn't have an Video-typed subitem",
["libraryItem"],
libraryItemURL
);
}
} else if (
libraryItem.metadata[0].subtype.data.attributes.slug === "item-set"
) {
if (libraryItem.subitems.data.length === 0) {
prettyTestError(
router,
"Item-set item should have subitems",
["libraryItem"],
libraryItemURL
);
}
}
}
if (!libraryItem.price) {
prettyTestWarning(
router,
"Missing price",
["libraryItem"],
libraryItemURL
);
} else {
if (!libraryItem.price.amount) {
prettyTestError(
router,
"Missing amount",
["libraryItem", "price"],
libraryItemURL
);
}
if (!libraryItem.price.currency) {
prettyTestError(
router,
"Missing currency",
["libraryItem", "price"],
libraryItemURL
);
}
}
if (!libraryItem.digital) {
if (!libraryItem.size) {
prettyTestWarning(
router,
"Missing size",
["libraryItem"],
libraryItemURL
);
} else {
if (!libraryItem.size.width) {
prettyTestWarning(
router,
"Missing width",
["libraryItem", "size"],
libraryItemURL
);
}
if (!libraryItem.size.height) {
prettyTestWarning(
router,
"Missing height",
["libraryItem", "size"],
libraryItemURL
);
}
if (!libraryItem.size.thickness) {
prettyTestWarning(
router,
"Missing thickness",
["libraryItem", "size"],
libraryItemURL
);
}
}
}
if (!libraryItem.release_date) {
prettyTestWarning(
router,
"Missing release_date",
["libraryItem"],
libraryItemURL
);
} else {
if (!libraryItem.release_date.year) {
prettyTestError(
router,
"Missing year",
["libraryItem", "release_date"],
libraryItemURL
);
}
if (!libraryItem.release_date.month) {
prettyTestError(
router,
"Missing month",
["libraryItem", "release_date"],
libraryItemURL
);
}
if (!libraryItem.release_date.day) {
prettyTestError(
router,
"Missing day",
["libraryItem", "release_date"],
libraryItemURL
);
}
}
if (libraryItem.contents.data.length === 0) {
prettyTestWarning(
router,
"Missing contents",
["libraryItem"],
libraryItemURL
);
} else {
let currentRangePage = 0;
libraryItem.contents.data.map((content) => {
const contentURL =
"/admin/content-manager/collectionType/api::content.content/" +
content.id;
if (content.attributes.scan_set.length === 0) {
prettyTestWarning(
router,
"Missing scan_set",
["libraryItem", "content", content.id],
contentURL
);
}
if (content.attributes.range.length === 0) {
prettyTestWarning(
router,
"Missing range",
["libraryItem", "content", content.id],
contentURL
);
} else if (
content.attributes.range[0].__typename ===
"ComponentRangePageRange"
) {
if (
content.attributes.range[0].starting_page <
currentRangePage + 1
) {
prettyTestError(
router,
`Overlapping pages ${content.attributes.range[0].starting_page} to ${currentRangePage}`,
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
} else if (
content.attributes.range[0].starting_page >
currentRangePage + 1
) {
prettyTestError(
router,
`Missing pages ${currentRangePage + 1} to ${
content.attributes.range[0].starting_page - 1
}`,
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
}
if (!content.attributes.content.data) {
prettyTestWarning(
router,
"Missing content",
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
}
currentRangePage = content.attributes.range[0].ending_page;
}
});
if (libraryItem.metadata[0].__typename === "ComponentMetadataBooks") {
if (currentRangePage < libraryItem.metadata[0].page_count) {
prettyTestError(
router,
`Missing pages ${currentRangePage + 1} to ${
libraryItem.metadata[0].page_count
}`,
["libraryItem", "content"],
libraryItemURL
);
} else if (currentRangePage > libraryItem.metadata[0].page_count) {
prettyTestError(
router,
`Page overflow, content references pages up to ${currentRangePage} when the highest expected was ${libraryItem.metadata[0].page_count}`,
["libraryItem", "content"],
libraryItemURL
);
}
if (libraryItem.metadata[0].languages.data.length === 0) {
prettyTestWarning(
router,
"Missing language",
["libraryItem", "metadata"],
libraryItemURL
);
}
if (!libraryItem.metadata[0].page_count) {
prettyTestWarning(
router,
"Missing page_count",
["libraryItem", "metadata"],
libraryItemURL
);
}
}
}
}
}
if (!libraryItem.root_item && libraryItem.subitem_of.data.length === 0) {
prettyTestError(
router,
"This item is inaccessible (not root item and not subitem of another item)",
["libraryItem"],
libraryItemURL
);
}
if (libraryItem.gallery.data.length === 0) {
prettyTestWarning(
router,
"Missing gallery",
["libraryItem"],
libraryItemURL
);
}
}
if (libraryItem.descriptions.length === 0) {
prettyTestWarning(
router,
"Missing description",
["libraryItem"],
libraryItemURL
);
}
}

View File

@ -0,0 +1,766 @@
import AppLayout from "components/AppLayout";
import Button from "components/Button";
import Chip from "components/Chip";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import InsetBox from "components/InsetBox";
import ContentTOCLine from "components/Library/ContentTOCLine";
import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
import LightBox from "components/LightBox";
import NavOption from "components/PanelComponents/NavOption";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext";
import { getLibraryItem, getLibraryItemsSlugs } from "graphql/operations";
import {
Enum_Componentmetadatabooks_Binding_Type,
Enum_Componentmetadatabooks_Page_Order,
GetLibraryItemQuery,
} from "graphql/operations-types";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
convertMmToInch,
prettyDate,
prettyinlineTitle,
prettyItemSubType,
prettyItemType,
prettyPrice,
prettyTestError,
prettyTestWarning,
sortContent,
} from "queries/helpers";
import { useState } from "react";
interface LibrarySlugProps extends AppStaticProps {
item: GetLibraryItemQuery["libraryItems"]["data"][number]["attributes"];
itemId: GetLibraryItemQuery["libraryItems"]["data"][number]["id"];
}
export default function LibrarySlug(props: LibrarySlugProps): JSX.Element {
useTesting(props);
const { item, langui, currencies } = props;
const appLayout = useAppLayout();
const isVariantSet =
item.metadata.length > 0 &&
item.metadata[0].__typename === "ComponentMetadataGroup" &&
item.metadata[0].subtype.data.attributes.slug === "variant-set";
sortContent(item.contents);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
const subPanel = (
<SubPanel>
<ReturnButton
href="/library/"
title={langui.library}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
<div className="grid gap-4">
<NavOption
title={langui.summary}
url="#summary"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
{item.gallery.data.length > 0 && (
<NavOption
title={langui.gallery}
url="#gallery"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
)}
<NavOption
title={langui.details}
url="#details"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
{item.subitems.data.length > 0 && (
<NavOption
title={isVariantSet ? langui.variants : langui.subitems}
url={isVariantSet ? "#variants" : "#subitems"}
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
)}
{item.contents.data.length > 0 && (
<NavOption
title={langui.contents}
url="#contents"
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
)}
</div>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<LightBox
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<ReturnButton
href="/library/"
title={langui.library}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<div className="grid place-items-center gap-12">
<div
className="drop-shadow-shade-xl w-full h-[50vh] mobile:h-[60vh] desktop:mb-16 relative cursor-pointer"
onClick={() => {
setLightboxOpen(true);
setLightboxImages([
getAssetURL(
item.thumbnail.data.attributes.url,
ImageQuality.Large
),
]);
setLightboxIndex(0);
}}
>
{item.thumbnail.data ? (
<Img
image={item.thumbnail.data.attributes}
quality={ImageQuality.Large}
layout="fill"
objectFit="contain"
priority
/>
) : (
<div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div>
)}
</div>
<InsetBox id="summary" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8">
{item.subitem_of.data.length > 0 && (
<div className="grid place-items-center">
<p>{langui.subitem_of}</p>
<Button
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
>
{prettyinlineTitle(
"",
item.subitem_of.data[0].attributes.title,
item.subitem_of.data[0].attributes.subtitle
)}
</Button>
</div>
)}
<div className="grid place-items-center">
<h1 className="text-3xl">{item.title}</h1>
{item.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>}
</div>
{item.descriptions.length > 0 && (
<p className="text-justify">{item.descriptions[0].description}</p>
)}
</div>
</InsetBox>
{item.gallery.data.length > 0 && (
<div id="gallery" className="grid place-items-center gap-8 w-full">
<h2 className="text-2xl">{langui.gallery}</h2>
<div className="grid w-full gap-8 items-end grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]">
{item.gallery.data.map((galleryItem, index) => (
<div
key={galleryItem.id}
className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer"
onClick={() => {
setLightboxOpen(true);
setLightboxImages(
item.gallery.data.map((image) =>
getAssetURL(image.attributes.url, ImageQuality.Large)
)
);
setLightboxIndex(index);
}}
>
<div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div>
<Img
className="rounded-lg"
image={galleryItem.attributes}
layout="fill"
objectFit="cover"
/>
</div>
))}
</div>
</div>
)}
<InsetBox id="details" className="grid place-items-center">
<div className="w-[clamp(0px,100%,42rem)] grid place-items gap-8">
<h2 className="text-2xl text-center">{langui.details}</h2>
<div className="grid grid-flow-col w-full place-content-between">
{item.metadata.length > 0 && (
<div className="grid place-items-center place-content-start">
<h3 className="text-xl">{langui.type}</h3>
<div className="grid grid-flow-col gap-1">
<Chip>{prettyItemType(item.metadata[0], langui)}</Chip>
{""}
<Chip>{prettyItemSubType(item.metadata[0])}</Chip>
</div>
</div>
)}
{item.release_date && (
<div className="grid place-items-center place-content-start">
<h3 className="text-xl">{langui.release_date}</h3>
<p>{prettyDate(item.release_date)}</p>
</div>
)}
{item.price && (
<div className="grid place-items-center text-center place-content-start">
<h3 className="text-xl">{langui.price}</h3>
<p>
{prettyPrice(
item.price,
currencies,
item.price.currency.data.attributes.code
)}
</p>
{item.price.currency.data.attributes.code !==
appLayout.currency && (
<p>
{prettyPrice(item.price, currencies, appLayout.currency)}{" "}
<br />({langui.calculated?.toLowerCase()})
</p>
)}
</div>
)}
</div>
{item.categories.data.length > 0 && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{item.categories.data.map((category) => (
<Chip key={category.id}>{category.attributes.name}</Chip>
))}
</div>
</div>
)}
{item.size && (
<>
<h3 className="text-xl">{langui.size}</h3>
<div className="grid grid-flow-col w-full place-content-between">
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.width}:</p>
<div>
<p>{item.size.width} mm</p>
<p>{convertMmToInch(item.size.width)} in</p>
</div>
</div>
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.height}:</p>
<div>
<p>{item.size.height} mm</p>
<p>{convertMmToInch(item.size.height)} in</p>
</div>
</div>
{item.size.thickness && (
<div className="flex flex-row flex-wrap place-items-start gap-4">
<p className="font-bold">{langui.thickness}:</p>
<div>
<p>{item.size.thickness} mm</p>
<p>{convertMmToInch(item.size.thickness)} in</p>
</div>
</div>
)}
</div>
</>
)}
{item.metadata.length > 0 &&
item.metadata[0].__typename !== "ComponentMetadataGroup" &&
item.metadata[0].__typename !== "ComponentMetadataOther" && (
<>
<h3 className="text-xl">{langui.type_information}</h3>
<div className="grid grid-cols-2 w-full place-content-between">
{item.metadata[0].__typename ===
"ComponentMetadataBooks" && (
<>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.pages}:</p>
<p>{item.metadata[0].page_count}</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.binding}:</p>
<p>
{item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Paperback
? langui.paperback
: item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Hardcover
? langui.hardcover
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.page_order}:</p>
<p>
{item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.LeftToRight
? langui.left_to_right
: item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.RightToLeft
? langui.right_to_left
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.languages}:</p>
{item.metadata[0].languages.data.map((lang) => (
<p key={lang.attributes.code}>
{lang.attributes.name}
</p>
))}
</div>
</>
)}
</div>
</>
)}
</div>
</InsetBox>
{item.subitems.data.length > 0 && (
<div
id={isVariantSet ? "variants" : "subitems"}
className="grid place-items-center gap-8 w-full"
>
<h2 className="text-2xl">
{isVariantSet ? langui.variants : langui.subitems}
</h2>
<div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full">
{item.subitems.data.map((subitem) => (
<LibraryItemsPreview
key={subitem.id}
item={subitem.attributes}
/>
))}
</div>
</div>
)}
{item.contents.data.length > 0 && (
<div id="contents" className="w-full grid place-items-center gap-8">
<h2 className="text-2xl">{langui.contents}</h2>
<div className="grid gap-4 w-full">
{item.contents.data.map((content) => (
<ContentTOCLine
langui={langui}
content={content}
parentSlug={item.slug}
key={content.id}
/>
))}
</div>
</div>
)}
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.library}
title={prettyinlineTitle("", item.title, item.subtitle)}
contentPanel={contentPanel}
subPanel={subPanel}
thumbnail={item.thumbnail.data?.attributes}
description={
item.descriptions.length > 0
? item.descriptions[0].description
: undefined
}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: LibrarySlugProps }> {
const item = (
await getLibraryItem({
slug: context.params?.slug?.toString() ?? "",
language_code: context.locale ?? "en",
})
).libraryItems.data[0];
const props: LibrarySlugProps = {
...(await getAppStaticProps(context)),
item: item.attributes,
itemId: item.id,
};
return {
props: props,
};
}
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const libraryItems = await getLibraryItemsSlugs({});
const paths: GetStaticPathsResult["paths"] = [];
libraryItems.libraryItems.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
});
return {
paths,
fallback: false,
};
}
function useTesting(props: LibrarySlugProps) {
const { item, itemId } = props;
const router = useRouter();
const libraryItemURL = `/admin/content-manager/collectionType/api::library-item.library-item/${itemId}`;
sortContent(item.contents);
if (router.locale === "en") {
if (!item.thumbnail.data) {
prettyTestError(
router,
"Missing thumbnail",
["libraryItem"],
libraryItemURL
);
}
if (item.metadata.length === 0) {
prettyTestError(
router,
"Missing metadata",
["libraryItem"],
libraryItemURL
);
} else if (
item.metadata[0].__typename === "ComponentMetadataGroup" &&
(item.metadata[0].subtype.data.attributes.slug === "relation-set" ||
item.metadata[0].subtype.data.attributes.slug === "variant-set")
) {
// This is a group type item
if (item.price) {
prettyTestError(
router,
"Group-type items shouldn't have price",
["libraryItem"],
libraryItemURL
);
}
if (item.size) {
prettyTestError(
router,
"Group-type items shouldn't have size",
["libraryItem"],
libraryItemURL
);
}
if (item.release_date) {
prettyTestError(
router,
"Group-type items shouldn't have release_date",
["libraryItem"],
libraryItemURL
);
}
if (item.contents.data.length > 0) {
prettyTestError(
router,
"Group-type items shouldn't have contents",
["libraryItem"],
libraryItemURL
);
}
if (item.subitems.data.length === 0) {
prettyTestError(
router,
"Group-type items should have subitems",
["libraryItem"],
libraryItemURL
);
}
} else {
// This is a normal item
if (item.metadata[0].__typename === "ComponentMetadataGroup") {
if (item.subitems.data.length === 0) {
prettyTestError(
router,
"Group-type item should have subitems",
["libraryItem"],
libraryItemURL
);
}
}
if (item.price) {
if (!item.price.amount) {
prettyTestError(
router,
"Missing amount",
["libraryItem", "price"],
libraryItemURL
);
}
if (!item.price.currency) {
prettyTestError(
router,
"Missing currency",
["libraryItem", "price"],
libraryItemURL
);
}
} else {
prettyTestWarning(
router,
"Missing price",
["libraryItem"],
libraryItemURL
);
}
if (!item.digital) {
if (item.size) {
if (!item.size.width) {
prettyTestWarning(
router,
"Missing width",
["libraryItem", "size"],
libraryItemURL
);
}
if (!item.size.height) {
prettyTestWarning(
router,
"Missing height",
["libraryItem", "size"],
libraryItemURL
);
}
if (!item.size.thickness) {
prettyTestWarning(
router,
"Missing thickness",
["libraryItem", "size"],
libraryItemURL
);
}
} else {
prettyTestWarning(
router,
"Missing size",
["libraryItem"],
libraryItemURL
);
}
}
if (item.release_date) {
if (!item.release_date.year) {
prettyTestError(
router,
"Missing year",
["libraryItem", "release_date"],
libraryItemURL
);
}
if (!item.release_date.month) {
prettyTestError(
router,
"Missing month",
["libraryItem", "release_date"],
libraryItemURL
);
}
if (!item.release_date.day) {
prettyTestError(
router,
"Missing day",
["libraryItem", "release_date"],
libraryItemURL
);
}
} else {
prettyTestWarning(
router,
"Missing release_date",
["libraryItem"],
libraryItemURL
);
}
if (item.contents.data.length === 0) {
prettyTestWarning(
router,
"Missing contents",
["libraryItem"],
libraryItemURL
);
} else {
let currentRangePage = 0;
item.contents.data.map((content) => {
const contentURL = `/admin/content-manager/collectionType/api::content.content/${content.id}`;
if (content.attributes.scan_set.length === 0) {
prettyTestWarning(
router,
"Missing scan_set",
["libraryItem", "content", content.id],
contentURL
);
}
if (content.attributes.range.length === 0) {
prettyTestWarning(
router,
"Missing range",
["libraryItem", "content", content.id],
contentURL
);
} else if (
content.attributes.range[0].__typename === "ComponentRangePageRange"
) {
if (
content.attributes.range[0].starting_page <
currentRangePage + 1
) {
prettyTestError(
router,
`Overlapping pages ${content.attributes.range[0].starting_page} to ${currentRangePage}`,
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
} else if (
content.attributes.range[0].starting_page >
currentRangePage + 1
) {
prettyTestError(
router,
`Missing pages ${currentRangePage + 1} to ${
content.attributes.range[0].starting_page - 1
}`,
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
}
if (!content.attributes.content.data) {
prettyTestWarning(
router,
"Missing content",
["libraryItem", "content", content.id, "range"],
libraryItemURL
);
}
currentRangePage = content.attributes.range[0].ending_page;
}
});
if (item.metadata[0].__typename === "ComponentMetadataBooks") {
if (currentRangePage < item.metadata[0].page_count) {
prettyTestError(
router,
`Missing pages ${currentRangePage + 1} to ${
item.metadata[0].page_count
}`,
["libraryItem", "content"],
libraryItemURL
);
} else if (currentRangePage > item.metadata[0].page_count) {
prettyTestError(
router,
`Page overflow, content references pages up to ${currentRangePage} when the highest expected was ${item.metadata[0].page_count}`,
["libraryItem", "content"],
libraryItemURL
);
}
if (item.metadata[0].languages.data.length === 0) {
prettyTestWarning(
router,
"Missing language",
["libraryItem", "metadata"],
libraryItemURL
);
}
if (!item.metadata[0].page_count) {
prettyTestWarning(
router,
"Missing page_count",
["libraryItem", "metadata"],
libraryItemURL
);
}
}
}
}
if (!item.root_item && item.subitem_of.data.length === 0) {
prettyTestError(
router,
"This item is inaccessible (not root item and not subitem of another item)",
["libraryItem"],
libraryItemURL
);
}
if (item.gallery.data.length === 0) {
prettyTestWarning(
router,
"Missing gallery",
["libraryItem"],
libraryItemURL
);
}
}
if (item.descriptions.length === 0) {
prettyTestWarning(
router,
"Missing description",
["libraryItem"],
libraryItemURL
);
}
}

View File

@ -0,0 +1,180 @@
import AppLayout from "components/AppLayout";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import LanguageSwitcher from "components/LanguageSwitcher";
import LightBox from "components/LightBox";
import NavOption from "components/PanelComponents/NavOption";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext";
import { getLibraryItemScans, getLibraryItemsSlugs } from "graphql/operations";
import { GetLibraryItemScansQuery } from "graphql/operations-types";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyinlineTitle, prettySlug, sortContent } from "queries/helpers";
import { useState } from "react";
interface Props extends AppStaticProps {
item: GetLibraryItemScansQuery["libraryItems"]["data"][number]["attributes"];
itemId: GetLibraryItemScansQuery["libraryItems"]["data"][number]["id"];
}
export default function LibrarySlug(props: Props): JSX.Element {
const { item, langui } = props;
const appLayout = useAppLayout();
sortContent(item.contents);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [lightboxImages, setLightboxImages] = useState([""]);
const [lightboxIndex, setLightboxIndex] = useState(0);
const subPanel = (
<SubPanel>
<ReturnButton
href={`/library/${item.slug}`}
title={langui.item}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
{item.contents.data.map((content) => (
<NavOption
key={content.id}
url={`#${content.attributes.slug}`}
title={prettySlug(content.attributes.slug, item.slug)}
subtitle={
content.attributes.range.length > 0 &&
content.attributes.range[0].__typename === "ComponentRangePageRange"
? `${content.attributes.range[0].starting_page}${content.attributes.range[0].ending_page}`
: undefined
}
onClick={() => appLayout.setSubPanelOpen(false)}
border
/>
))}
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<LightBox
state={lightboxOpen}
setState={setLightboxOpen}
images={lightboxImages}
index={lightboxIndex}
setIndex={setLightboxIndex}
/>
<ReturnButton
href={`/library/${item.slug}`}
title={langui.item}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
{item.contents.data.map((content) => (
<>
<h2
id={content.attributes.slug}
key={`h2${content.id}`}
className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2"
>
{prettySlug(content.attributes.slug, item.slug)}
</h2>
{content.attributes.scan_set.length > 0 ? (
<div
key={`items${content.id}`}
className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{content.attributes.scan_set[0].pages.data.map((page, index) => (
<div
key={page.id}
className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform"
onClick={() => {
setLightboxOpen(true);
setLightboxImages(
content.attributes.scan_set[0].pages.data.map((image) =>
getAssetURL(image.attributes.url, ImageQuality.Large)
)
);
setLightboxIndex(index);
}}
>
<Img image={page.attributes} quality={ImageQuality.Small} />
</div>
))}
</div>
) : (
<div className="pb-12 border-b-[3px] border-dotted last-of-type:border-0">
<LanguageSwitcher
locales={content.attributes.scan_set_languages.map(
(language) => language.language.data.attributes.code
)}
languages={props.languages}
langui={props.langui}
href={`#${content.attributes.slug}`}
/>
</div>
)}
</>
))}
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.library}
title={prettyinlineTitle("", item.title, item.subtitle)}
contentPanel={contentPanel}
subPanel={subPanel}
thumbnail={item.thumbnail.data?.attributes}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: Props }> {
const item = (
await getLibraryItemScans({
slug: context.params?.slug?.toString() ?? "",
language_code: context.locale ?? "en",
})
).libraryItems.data[0];
const props: Props = {
...(await getAppStaticProps(context)),
item: item.attributes,
itemId: item.id,
};
return {
props: props,
};
}
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const libraryItems = await getLibraryItemsSlugs({});
const paths: GetStaticPathsResult["paths"] = [];
libraryItems.libraryItems.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
});
return {
paths,
fallback: false,
};
}

View File

@ -1,69 +1,390 @@
import { GetStaticProps } from "next";
import SubPanel from "components/Panels/SubPanel";
import AppLayout from "components/AppLayout";
import Chip from "components/Chip";
import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
import PanelHeader from "components/PanelComponents/PanelHeader";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import Select from "components/Select";
import Switch from "components/Switch";
import { getLibraryItemsPreview } from "graphql/operations";
import {
GetCurrenciesQuery,
GetLibraryItemsPreviewQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import {
getLibraryItemsPreview,
getWebsiteInterface,
} from "graphql/operations";
import PanelHeader from "components/PanelComponents/PanelHeader";
import AppLayout from "components/AppLayout";
import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { convertPrice, prettyDate, prettyinlineTitle } from "queries/helpers";
import { useEffect, useState } from "react";
type LibraryProps = {
libraryItems: GetLibraryItemsPreviewQuery;
langui: GetWebsiteInterfaceQuery;
};
interface LibraryProps extends AppStaticProps {
items: GetLibraryItemsPreviewQuery["libraryItems"]["data"];
}
type GroupLibraryItems = Map<
string,
GetLibraryItemsPreviewQuery["libraryItems"]["data"]
>;
export default function Library(props: LibraryProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui, items: libraryItems, currencies } = props;
const [showSubitems, setShowSubitems] = useState<boolean>(false);
const [showPrimaryItems, setShowPrimaryItems] = useState<boolean>(true);
const [showSecondaryItems, setShowSecondaryItems] = useState<boolean>(false);
const [sortingMethod, setSortingMethod] = useState<number>(0);
const [groupingMethod, setGroupingMethod] = useState<number>(-1);
const [filteredItems, setFilteredItems] = useState<LibraryProps["items"]>(
filterItems(
showSubitems,
showPrimaryItems,
showSecondaryItems,
libraryItems
)
);
const [sortedItems, setSortedItem] = useState<LibraryProps["items"]>(
sortBy(groupingMethod, filteredItems, currencies)
);
const [groups, setGroups] = useState<GroupLibraryItems>(
getGroups(langui, groupingMethod, sortedItems)
);
useEffect(() => {
setFilteredItems(
filterItems(
showSubitems,
showPrimaryItems,
showSecondaryItems,
libraryItems
)
);
}, [showSubitems, libraryItems, showPrimaryItems, showSecondaryItems]);
useEffect(() => {
setSortedItem(sortBy(sortingMethod, filteredItems, currencies));
}, [currencies, filteredItems, sortingMethod]);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, sortedItems));
}, [langui, groupingMethod, sortedItems]);
const subPanel = (
<SubPanel>
<PanelHeader
icon="library_books"
title={langui.main_library}
title={langui.library}
description={langui.library_description}
/>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.group_by}:</p>
<Select
className="w-full"
options={[langui.category, langui.type, langui.release_year]}
state={groupingMethod}
setState={setGroupingMethod}
allowEmpty
/>
</div>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.order_by}:</p>
<Select
className="w-full"
options={[langui.name, langui.price, langui.release_date]}
state={sortingMethod}
setState={setSortingMethod}
/>
</div>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.show_subitems}:</p>
<Switch state={showSubitems} setState={setShowSubitems} />
</div>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.show_primary_items}:</p>
<Switch state={showPrimaryItems} setState={setShowPrimaryItems} />
</div>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">{langui.show_secondary_items}:</p>
<Switch state={showSecondaryItems} setState={setShowSecondaryItems} />
</div>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]">
{props.libraryItems.libraryItems.data.map((item) => (
<LibraryItemsPreview key={item.id} item={item.attributes} />
))}
</div>
{[...groups].map(([name, items]) => (
<>
{items.length > 0 && (
<>
{name && (
<h2
key={`h2${name}`}
className="text-2xl pb-2 pt-10 first-of-type:pt-0 flex flex-row place-items-center gap-2"
>
{name}
<Chip>{`${items.length} ${
items.length <= 1
? langui.result.toLowerCase()
: langui.results.toLowerCase()
}`}</Chip>
</h2>
)}
<div
key={`items${name}`}
className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0"
>
{items.map((item) => (
<LibraryItemsPreview
key={item.id}
item={item.attributes}
currencies={props.currencies}
/>
))}
</div>
</>
)}
</>
))}
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.main_library}
langui={langui}
navTitle={langui.library}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: LibraryProps = {
libraryItems: await getLibraryItemsPreview({
language_code: context.locale,
}),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
} else {
return { props: {} };
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: LibraryProps }> {
const props: LibraryProps = {
...(await getAppStaticProps(context)),
items: (
await getLibraryItemsPreview({
language_code: context.locale ?? "en",
})
).libraryItems.data,
};
return {
props: props,
};
}
function getGroups(
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"],
groupByType: number,
items: LibraryProps["items"]
): GroupLibraryItems {
switch (groupByType) {
case 0: {
const typeGroup = new Map();
typeGroup.set("Drakengard 1", []);
typeGroup.set("Drakengard 1.3", []);
typeGroup.set("Drakengard 2", []);
typeGroup.set("Drakengard 3", []);
typeGroup.set("Drakengard 4", []);
typeGroup.set("NieR Gestalt", []);
typeGroup.set("NieR Replicant", []);
typeGroup.set("NieR Replicant ver.1.22474487139...", []);
typeGroup.set("NieR:Automata", []);
typeGroup.set("NieR Re[in]carnation", []);
typeGroup.set("SINoALICE", []);
typeGroup.set("Voice of Cards", []);
typeGroup.set("Final Fantasy XIV", []);
typeGroup.set("Thou Shalt Not Die", []);
typeGroup.set("Bakuken", []);
typeGroup.set("YoRHa", []);
typeGroup.set("YoRHa Boys", []);
typeGroup.set(langui.no_category, []);
items.map((item) => {
if (item.attributes.categories.data.length === 0) {
typeGroup.get(langui.no_category)?.push(item);
} else {
item.attributes.categories.data.map((category) => {
typeGroup.get(category.attributes.name)?.push(item);
});
}
});
return typeGroup;
}
case 1: {
const group: GroupLibraryItems = new Map();
group.set(langui.audio, []);
group.set(langui.game, []);
group.set(langui.textual, []);
group.set(langui.video, []);
group.set(langui.other, []);
group.set(langui.group, []);
group.set(langui.no_type, []);
items.map((item) => {
if (item.attributes.metadata.length > 0) {
switch (item.attributes.metadata[0].__typename) {
case "ComponentMetadataAudio":
group.get(langui.audio)?.push(item);
break;
case "ComponentMetadataGame":
group.get(langui.game)?.push(item);
break;
case "ComponentMetadataBooks":
group.get(langui.textual)?.push(item);
break;
case "ComponentMetadataVideo":
group.get(langui.video)?.push(item);
break;
case "ComponentMetadataOther":
group.get(langui.other)?.push(item);
break;
case "ComponentMetadataGroup":
switch (
item.attributes.metadata[0].subitems_type.data.attributes.slug
) {
case "audio":
group.get(langui.audio)?.push(item);
break;
case "video":
group.get(langui.video)?.push(item);
break;
case "game":
group.get(langui.game)?.push(item);
break;
case "textual":
group.get(langui.textual)?.push(item);
break;
case "mixed":
group.get(langui.group)?.push(item);
break;
default: {
throw new Error(
"An unexpected subtype of group-metadata was given"
);
}
}
break;
default: {
throw new Error("An unexpected type of metadata was given");
}
}
} else {
group.get(langui.no_type)?.push(item);
}
});
return group;
}
case 2: {
const years: number[] = [];
items.map((item) => {
if (item.attributes.release_date) {
if (!years.includes(item.attributes.release_date.year))
years.push(item.attributes.release_date.year);
}
});
const group: GroupLibraryItems = new Map();
years.sort((a, b) => a - b);
years.map((year) => {
group.set(year.toString(), []);
});
group.set(langui.no_year, []);
items.map((item) => {
if (item.attributes.release_date) {
group.get(item.attributes.release_date.year.toString())?.push(item);
} else {
group.get(langui.no_year)?.push(item);
}
});
return group;
}
default: {
const group: GroupLibraryItems = new Map();
group.set("", items);
return group;
}
}
};
}
function filterItems(
showSubitems: boolean,
showPrimaryItems: boolean,
showSecondaryItems: boolean,
items: LibraryProps["items"]
): LibraryProps["items"] {
return [...items].filter((item) => {
if (!showSubitems && !item.attributes.root_item) return false;
if (
showSubitems &&
item.attributes.metadata.length > 0 &&
item.attributes.metadata[0].__typename === "ComponentMetadataGroup" &&
(item.attributes.metadata[0].subtype.data.attributes.slug ===
"variant-set" ||
item.attributes.metadata[0].subtype.data.attributes.slug ===
"relation-set")
)
return false;
if (item.attributes.primary && !showPrimaryItems) return false;
if (!item.attributes.primary && !showSecondaryItems) return false;
return true;
});
}
function sortBy(
orderByType: number,
items: LibraryProps["items"],
currencies: GetCurrenciesQuery["currencies"]["data"]
): LibraryProps["items"] {
switch (orderByType) {
case 0:
return [...items].sort((a, b) => {
const titleA = prettyinlineTitle(
"",
a.attributes.title,
a.attributes.subtitle
);
const titleB = prettyinlineTitle(
"",
b.attributes.title,
b.attributes.subtitle
);
return titleA.localeCompare(titleB);
});
case 1:
return [...items].sort((a, b) => {
const priceA = a.attributes.price
? convertPrice(a.attributes.price, currencies[0])
: 99999;
const priceB = b.attributes.price
? convertPrice(b.attributes.price, currencies[0])
: 99999;
return priceA - priceB;
});
case 2:
return [...items].sort((a, b) => {
const dateA =
a.attributes.release_date === null
? "9999"
: prettyDate(a.attributes.release_date);
const dateB =
b.attributes.release_date === null
? "9999"
: prettyDate(b.attributes.release_date);
return dateA.localeCompare(dateB);
});
default:
return items;
}
}

View File

@ -1,45 +1,32 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import AppLayout from "components/AppLayout";
import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type MerchProps = {
langui: GetWebsiteInterfaceQuery;
};
interface MerchProps extends AppStaticProps {}
export default function Merch(props: MerchProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui } = props;
const subPanel = (
<SubPanel>
<PanelHeader
icon="store"
title={langui.main_merch}
title={langui.merch}
description={langui.merch_description}
/>
</SubPanel>
);
return (
<AppLayout
navTitle={langui.main_merch}
langui={langui}
subPanel={subPanel}
/>
);
return <AppLayout navTitle={langui.merch} subPanel={subPanel} {...props} />;
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: MerchProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: MerchProps }> {
const props: MerchProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

181
src/pages/news/[slug].tsx Normal file
View File

@ -0,0 +1,181 @@
import AppLayout from "components/AppLayout";
import Chip from "components/Chip";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import HorizontalLine from "components/HorizontalLine";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { getPost, getPostsSlugs } from "graphql/operations";
import { GetPostQuery, StrapiImage } from "graphql/operations-types";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
getLocalesFromLanguages,
getStatusDescription,
prettySlug,
} from "queries/helpers";
interface PostProps extends AppStaticProps {
post: GetPostQuery["posts"]["data"][number]["attributes"];
postId: GetPostQuery["posts"]["data"][number]["id"];
}
export default function LibrarySlug(props: PostProps): JSX.Element {
const { post, langui } = props;
const locales = getLocalesFromLanguages(post.translations_languages);
const router = useRouter();
const thumbnail: StrapiImage | undefined =
post.translations.length > 0 && post.translations[0].thumbnail.data
? post.translations[0].thumbnail.data.attributes
: post.thumbnail.data
? post.thumbnail.data.attributes
: undefined;
const subPanel = (
<SubPanel>
<ReturnButton
href="/news"
title={langui.news}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
{post.translations.length > 0 && (
<div className="grid grid-flow-col place-items-center place-content-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(post.translations[0].status, langui)}
maxWidth={"20rem"}
>
<Chip>{post.translations[0].status}</Chip>
</ToolTip>
</div>
)}
{post.authors.data.length > 0 && (
<div>
<p className="font-headers">{"Authors"}:</p>
<div className="grid place-items-center place-content-center gap-2">
{post.authors.data.map((author) => (
<RecorderChip key={author.id} langui={langui} recorder={author} />
))}
</div>
</div>
)}
<HorizontalLine />
{post.translations.length > 0 && post.translations[0].body && (
<TOC
text={post.translations[0].body}
title={post.translations[0].title}
/>
)}
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/news"
title={langui.news}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<ThumbnailHeader
thumbnail={thumbnail}
title={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
description={
post.translations.length > 0
? post.translations[0].excerpt
: undefined
}
langui={langui}
categories={post.categories}
/>
<HorizontalLine />
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={post.translations[0].body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.news}
title={
post.translations.length > 0
? post.translations[0].title
: prettySlug(post.slug)
}
contentPanel={contentPanel}
subPanel={subPanel}
thumbnail={thumbnail}
{...props}
/>
);
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: PostProps }> {
const slug = context.params?.slug?.toString() ?? "";
const post = (
await getPost({
slug: slug,
language_code: context.locale ?? "en",
})
).posts.data[0];
const props: PostProps = {
...(await getAppStaticProps(context)),
post: post.attributes,
postId: post.id,
};
return {
props: props,
};
}
export async function getStaticPaths(
context: GetStaticPathsContext
): Promise<GetStaticPathsResult> {
const posts = await getPostsSlugs({});
const paths: GetStaticPathsResult["paths"] = [];
posts.posts.data.map((item) => {
context.locales?.map((local) => {
paths.push({ params: { slug: item.attributes.slug }, locale: local });
});
});
return {
paths,
fallback: false,
};
}

View File

@ -1,45 +1,73 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import AppLayout from "components/AppLayout";
import PostsPreview from "components/News/PostsPreview";
import PanelHeader from "components/PanelComponents/PanelHeader";
import ContentPanel, {
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { getPostsPreview } from "graphql/operations";
import { GetPostsPreviewQuery } from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { prettyDate } from "queries/helpers";
type NewsProps = {
langui: GetWebsiteInterfaceQuery;
};
interface NewsProps extends AppStaticProps {
posts: GetPostsPreviewQuery["posts"]["data"];
}
export default function News(props: NewsProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
const { langui, posts } = props;
posts
.sort((a, b) => {
const dateA =
a.attributes.date === null ? "9999" : prettyDate(a.attributes.date);
const dateB =
b.attributes.date === null ? "9999" : prettyDate(b.attributes.date);
return dateA.localeCompare(dateB);
})
.reverse();
const subPanel = (
<SubPanel>
<PanelHeader
icon="feed"
title={langui.main_news}
title={langui.news}
description={langui.news_description}
/>
</SubPanel>
);
const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end grid-cols-1 desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]">
{posts.map((post) => (
<PostsPreview key={post.id} post={post.attributes} />
))}
</div>
</ContentPanel>
);
return (
<AppLayout
navTitle={langui.main_news}
langui={langui}
navTitle={langui.news}
subPanel={subPanel}
contentPanel={contentPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: NewsProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: NewsProps }> {
const props: NewsProps = {
...(await getAppStaticProps(context)),
posts: await (
await getPostsPreview({ language_code: context.locale ?? "en" })
).posts.data,
};
return {
props: props,
};
}

View File

@ -1,93 +1,91 @@
import { GetStaticProps } from "next";
import AppLayout from "components/AppLayout";
import ChronologyYearComponent from "components/Chronology/ChronologyYearComponent";
import InsetBox from "components/InsetBox";
import NavOption from "components/PanelComponents/NavOption";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import ChronologyYearComponent from "components/Chronology/ChronologyYearComponent";
import { useAppLayout } from "contexts/AppLayoutContext";
import { getChronologyItems, getEras } from "graphql/operations";
import {
GetChronologyItemsQuery,
GetErasQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import {
getEras,
getChronologyItems,
getWebsiteInterface,
} from "graphql/operations";
import NavOption from "components/PanelComponents/NavOption";
import ReturnButton from "components/PanelComponents/ReturnButton";
import HorizontalLine from "components/HorizontalLine";
import AppLayout from "components/AppLayout";
import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
prettySlug,
prettyTestError,
prettyTestWarning,
} from "queries/helpers";
import InsetBox from "components/InsetBox";
import { useRouter } from "next/router";
import ReactTooltip from "react-tooltip";
interface DataChronologyProps {
chronologyItems: GetChronologyItemsQuery;
chronologyEras: GetErasQuery;
langui: GetWebsiteInterfaceQuery;
interface ChronologyProps extends AppStaticProps {
chronologyItems: GetChronologyItemsQuery["chronologyItems"]["data"];
chronologyEras: GetErasQuery["chronologyEras"]["data"];
}
export default function DataChronology(
props: DataChronologyProps
): JSX.Element {
export default function Chronology(props: ChronologyProps): JSX.Element {
useTesting(props);
const langui = props.langui.websiteInterfaces.data[0].attributes;
const chronologyItems = props.chronologyItems.chronologyItems;
const chronologyEras = props.chronologyEras.chronologyEras;
const { chronologyItems, chronologyEras, langui } = props;
const appLayout = useAppLayout();
// Group by year the Chronology items
let chronologyItemYearGroups: GetChronologyItemsQuery["chronologyItems"]["data"][number][][][] =
const chronologyItemYearGroups: GetChronologyItemsQuery["chronologyItems"]["data"][number][][][] =
[];
chronologyEras.data.map(() => {
chronologyEras.map(() => {
chronologyItemYearGroups.push([]);
});
let currentChronologyEraIndex = 0;
chronologyItems.data.map((item) => {
chronologyItems.map((item) => {
if (
item.attributes.year >
chronologyEras.data[currentChronologyEraIndex].attributes.ending_year
chronologyEras[currentChronologyEraIndex].attributes.ending_year
) {
currentChronologyEraIndex++;
currentChronologyEraIndex += 1;
}
if (
!chronologyItemYearGroups[currentChronologyEraIndex].hasOwnProperty(
Object.prototype.hasOwnProperty.call(
chronologyItemYearGroups[currentChronologyEraIndex],
item.attributes.year
)
) {
chronologyItemYearGroups[currentChronologyEraIndex][
item.attributes.year
] = [item];
].push(item);
} else {
chronologyItemYearGroups[currentChronologyEraIndex][
item.attributes.year
].push(item);
] = [item];
}
});
const subPanel = (
<SubPanel>
<ReturnButton href="/data" title="Data" langui={langui} />
<HorizontalLine />
<ReturnButton
href="/wiki"
title={langui.wiki}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
{props.chronologyEras.chronologyEras.data.map((era) => (
{chronologyEras.map((era) => (
<NavOption
key={era.id}
url={"#" + era.attributes.slug}
url={`#${era.attributes.slug}`}
title={
era.attributes.title.length > 0
? era.attributes.title[0].title
: prettySlug(era.attributes.slug)
}
subtitle={
era.attributes.starting_year + " → " + era.attributes.ending_year
}
subtitle={`${era.attributes.starting_year}${era.attributes.ending_year}`}
border
onClick={() => appLayout.setSubPanelOpen(false)}
/>
))}
</SubPanel>
@ -95,20 +93,28 @@ export default function DataChronology(
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/wiki"
title={langui.wiki}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
{chronologyItemYearGroups.map((era, eraIndex) => (
<>
<InsetBox
id={chronologyEras.data[eraIndex].attributes.slug}
id={chronologyEras[eraIndex].attributes.slug}
className="grid text-center my-8 gap-4"
>
<h2 className="text-2xl">
{chronologyEras.data[eraIndex].attributes.title.length > 0
? chronologyEras.data[eraIndex].attributes.title[0].title
: prettySlug(chronologyEras.data[eraIndex].attributes.slug)}
{chronologyEras[eraIndex].attributes.title.length > 0
? chronologyEras[eraIndex].attributes.title[0].title
: prettySlug(chronologyEras[eraIndex].attributes.slug)}
</h2>
<p className="whitespace-pre-line ">
{chronologyEras.data[eraIndex].attributes.title.length > 0
? chronologyEras.data[eraIndex].attributes.title[0].description
{chronologyEras[eraIndex].attributes.title.length > 0
? chronologyEras[eraIndex].attributes.title[0].description
: ""}
</p>
</InsetBox>
@ -117,57 +123,47 @@ export default function DataChronology(
key={`${eraIndex}-${index}`}
year={items[0].attributes.year}
items={items}
langui={langui}
/>
))}
</>
))}
<ReactTooltip
id="ChronologyTooltip"
place="top"
type="light"
effect="solid"
delayShow={50}
clickable={true}
className="drop-shadow-shade-xl !opacity-100 mobile:after:!border-r-light !bg-light !rounded-lg desktop:after:!border-t-light text-left !text-black max-w-xs"
/>
</ContentPanel>
);
return (
<AppLayout
navTitle="Chronology"
langui={langui}
contentPanel={contentPanel}
subPanel={subPanel}
{...props}
/>
);
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: DataChronologyProps = {
chronologyItems: await getChronologyItems({
language_code: context.locale,
}),
chronologyEras: await getEras({ language_code: context.locale }),
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: ChronologyProps }> {
const props: ChronologyProps = {
...(await getAppStaticProps(context)),
chronologyItems: (
await getChronologyItems({
language_code: context.locale ?? "en",
})
).chronologyItems.data,
chronologyEras: (await getEras({ language_code: context.locale ?? "en" }))
.chronologyEras.data,
};
return {
props: props,
};
}
function useTesting({ chronologyItems, chronologyEras }: DataChronologyProps) {
function useTesting(props: ChronologyProps) {
const router = useRouter();
chronologyEras.chronologyEras.data.map((era) => {
const chronologyErasURL =
"/admin/content-manager/collectionType/api::chronology-era.chronology-era/" +
chronologyItems.chronologyItems.data[0].id;
const { chronologyItems, chronologyEras } = props;
chronologyEras.map((era) => {
const chronologyErasURL = `/admin/content-manager/collectionType/api::chronology-era.chronology-era/${chronologyItems[0].id}`;
if (era.attributes.title.length === 0) {
prettyTestError(
@ -201,21 +197,12 @@ function useTesting({ chronologyItems, chronologyEras }: DataChronologyProps) {
}
});
chronologyItems.chronologyItems.data.map((item) => {
const chronologyItemsURL =
"/admin/content-manager/collectionType/api::chronology-item.chronology-item/" +
chronologyItems.chronologyItems.data[0].id;
chronologyItems.map((item) => {
const chronologyItemsURL = `/admin/content-manager/collectionType/api::chronology-item.chronology-item/${chronologyItems[0].id}`;
const date = `${item.attributes.year}/${item.attributes.month}/${item.attributes.day}`;
if (!(item.attributes.events.length > 0)) {
prettyTestError(
router,
"No events for this date",
["chronologyItems", date],
chronologyItemsURL
);
} else {
if (item.attributes.events.length > 0) {
item.attributes.events.map((event) => {
if (!event.source.data) {
prettyTestError(
@ -234,6 +221,13 @@ function useTesting({ chronologyItems, chronologyEras }: DataChronologyProps) {
);
}
});
} else {
prettyTestError(
router,
"No events for this date",
["chronologyItems", date],
chronologyItemsURL
);
}
});
}

View File

@ -1,48 +1,35 @@
import SubPanel from "components/Panels/SubPanel";
import PanelHeader from "components/PanelComponents/PanelHeader";
import { GetWebsiteInterfaceQuery } from "graphql/operations-types";
import { GetStaticProps } from "next";
import { getWebsiteInterface } from "graphql/operations";
import ContentPanel from "components/Panels/ContentPanel";
import AppLayout from "components/AppLayout";
import NavOption from "components/PanelComponents/NavOption";
import PanelHeader from "components/PanelComponents/PanelHeader";
import SubPanel from "components/Panels/SubPanel";
import { GetStaticPropsContext } from "next";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
type WikiProps = {
langui: GetWebsiteInterfaceQuery;
};
interface WikiProps extends AppStaticProps {}
export default function Hubs(props: WikiProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes;
export default function Wiki(props: WikiProps): JSX.Element {
const { langui } = props;
const subPanel = (
<SubPanel>
<PanelHeader
icon="travel_explore"
title={langui.main_wiki}
title={langui.wiki}
description={langui.wiki_description}
/>
<NavOption title="Chronology" url="/wiki/chronology" border />
</SubPanel>
);
const contentPanel = <ContentPanel>Hello</ContentPanel>;
return (
<AppLayout
navTitle={langui.main_wiki}
langui={langui}
contentPanel={contentPanel}
subPanel={subPanel}
/>
);
return <AppLayout navTitle={langui.wiki} subPanel={subPanel} {...props} />;
}
export const getStaticProps: GetStaticProps = async (context) => {
if (context.locale) {
const props: WikiProps = {
langui: await getWebsiteInterface({
language_code: context.locale,
}),
};
return {
props: props,
};
}
return { props: {} };
};
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<{ props: WikiProps }> {
const props: WikiProps = {
...(await getAppStaticProps(context)),
};
return {
props: props,
};
}

View File

@ -0,0 +1,39 @@
import {
getCurrencies,
getLanguages,
getWebsiteInterface,
} from "graphql/operations";
import {
GetCurrenciesQuery,
GetLanguagesQuery,
GetWebsiteInterfaceQuery,
} from "graphql/operations-types";
import { GetStaticPropsContext } from "next";
export interface AppStaticProps {
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"];
currencies: GetCurrenciesQuery["currencies"]["data"];
languages: GetLanguagesQuery["languages"]["data"];
}
export async function getAppStaticProps(
context: GetStaticPropsContext
): Promise<AppStaticProps> {
const languages = (await getLanguages({})).languages.data;
languages.sort((a, b) =>
a.attributes.localized_name.localeCompare(b.attributes.localized_name)
);
const currencies = (await getCurrencies({})).currencies.data;
currencies.sort((a, b) => a.attributes.code.localeCompare(b.attributes.code));
return {
langui: (
await getWebsiteInterface({
language_code: context.locale ?? "en",
})
).websiteInterfaces.data[0].attributes,
currencies: currencies,
languages: languages,
};
}

View File

@ -4,6 +4,9 @@ import {
ImageQuality,
} from "components/Img";
import {
Enum_Componentsetstextset_Status,
GetCurrenciesQuery,
GetLanguagesQuery,
GetLibraryItemQuery,
GetLibraryItemsPreviewQuery,
GetWebsiteInterfaceQuery,
@ -14,21 +17,39 @@ import { NextRouter } from "next/router";
export function prettyDate(
datePicker: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["release_date"]
): string {
return (
datePicker.year +
"/" +
datePicker.month.toString().padStart(2, "0") +
"/" +
datePicker.day.toString().padStart(2, "0")
);
return `${datePicker.year}/${datePicker.month
.toString()
.padStart(2, "0")}/${datePicker.day.toString().padStart(2, "0")}`;
}
export function prettyPrice(
pricePicker: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["price"]
pricePicker: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["price"],
currencies: GetCurrenciesQuery["currencies"]["data"],
targetCurrencyCode?: string
): string {
if (!targetCurrencyCode) return "";
let result = "";
currencies.map((currency) => {
if (currency.attributes.code === targetCurrencyCode) {
const amountInTargetCurrency = convertPrice(pricePicker, currency);
result =
currency.attributes.symbol +
amountInTargetCurrency.toLocaleString(undefined, {
minimumFractionDigits: currency.attributes.display_decimals ? 2 : 0,
maximumFractionDigits: currency.attributes.display_decimals ? 2 : 0,
});
}
});
return result;
}
export function convertPrice(
pricePicker: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["price"],
targetCurrency: GetCurrenciesQuery["currencies"]["data"][number]
): number {
return (
pricePicker.currency.data.attributes.symbol +
pricePicker.amount.toLocaleString()
(pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
targetCurrency.attributes.rate_to_usd
);
}
@ -49,9 +70,9 @@ export function prettyinlineTitle(
subtitle: string
): string {
let result = "";
if (pretitle) result += pretitle + ": ";
if (pretitle) result += `${pretitle}: `;
result += title;
if (subtitle) result += " - " + subtitle;
if (subtitle) result += ` - ${subtitle}`;
return result;
}
@ -61,64 +82,71 @@ export function prettyItemType(
},
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"]
): string {
const type = metadata.__typename;
switch (metadata.__typename) {
case "ComponentMetadataAudio":
return langui.library_item_type_audio;
return langui.audio;
case "ComponentMetadataBooks":
return langui.library_item_type_textual;
return langui.textual;
case "ComponentMetadataGame":
return langui.library_item_type_game;
return langui.game;
case "ComponentMetadataVideo":
return langui.library_item_type_video;
return langui.video;
case "ComponentMetadataGroup":
return langui.group;
case "ComponentMetadataOther":
return langui.library_item_type_other;
return langui.other;
default:
return "";
}
}
export function prettyItemSubType(metadata: {
/* eslint-disable @typescript-eslint/no-explicit-any */
__typename: GetLibraryItemsPreviewQuery["libraryItems"]["data"][number]["attributes"]["metadata"][number]["__typename"];
subtype?: any;
platforms?: any;
subitems_type?: any;
}): string {
switch (metadata.__typename) {
case "ComponentMetadataAudio":
case "ComponentMetadataBooks":
case "ComponentMetadataVideo":
case "ComponentMetadataOther": {
return metadata.subtype.data.attributes.titles.length > 0
? metadata.subtype.data.attributes.titles[0].title
: prettySlug(metadata.subtype.data.attributes.slug);
}
case "ComponentMetadataGame":
return metadata.platforms.data.length > 0
? metadata.platforms.data[0].attributes.short
: "";
case "ComponentMetadataGroup": {
const firstPart =
metadata.subtype.data.attributes.titles.length > 0
? metadata.subtype.data.attributes.titles[0].title
: prettySlug(metadata.subtype.data.attributes.slug);
const secondPart =
metadata.subitems_type.data.attributes.titles.length > 0
? metadata.subitems_type.data.attributes.titles[0].title
: prettySlug(metadata.subitems_type.data.attributes.slug);
return `${secondPart} ${firstPart}`;
}
default:
return "";
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}
export function prettyLanguage(code: string): string {
switch (code) {
case "en":
return "English";
case "es":
return "Español";
case "fr":
return "Français";
case "ja":
return "日本語";
case "en":
return "English";
case "xx":
return "██";
default:
return code;
}
export function prettyLanguage(
code: string,
languages: GetLanguagesQuery["languages"]["data"]
): string {
let result = code;
languages.forEach((language) => {
if (language.attributes.code === code)
result = language.attributes.localized_name;
});
return result;
}
export function prettyTestWarning(
@ -153,17 +181,19 @@ function prettyTestWritter(
): void {
const line = [
level,
process.env.NEXT_PUBLIC_URL_SELF + "/" + locale + asPath,
`${process.env.NEXT_PUBLIC_URL_SELF}/${locale}${asPath}`,
locale,
subCategory?.join(" -> "),
message,
process.env.NEXT_PUBLIC_URL_CMS + url,
];
if (level === TestingLevel.Warning) {
console.warn(line.join("\t"));
} else {
console.error(line.join("\t"));
if (process.env.ENABLE_TESTING_LOG) {
if (level === TestingLevel.Warning) {
console.warn(line.join("\t"));
} else {
console.error(line.join("\t"));
}
}
}
@ -173,7 +203,7 @@ export function capitalizeString(string: string): string {
}
let words = string.split(" ");
words = words.map((word) => (word = capitalizeWord(word)));
words = words.map((word) => capitalizeWord(word));
return words.join(" ");
}
@ -202,9 +232,13 @@ export function getOgImage(quality: ImageQuality, image: StrapiImage): OgImage {
};
}
export function sortContent(
contents: GetLibraryItemQuery["libraryItems"]["data"][number]["attributes"]["contents"]
) {
export function sortContent(contents: {
data: {
attributes: {
range: GetLibraryItemQuery["libraryItems"]["data"][number]["attributes"]["contents"]["data"][number]["attributes"]["range"];
};
}[];
}) {
contents.data.sort((a, b) => {
if (
a.attributes.range[0].__typename === "ComponentRangePageRange" &&
@ -218,3 +252,63 @@ export function sortContent(
return 0;
});
}
export function getStatusDescription(
status: string,
langui: GetWebsiteInterfaceQuery["websiteInterfaces"]["data"][number]["attributes"]
): string {
switch (status) {
case Enum_Componentsetstextset_Status.Incomplete:
return langui.status_incomplete;
case Enum_Componentsetstextset_Status.Draft:
return langui.status_draft;
case Enum_Componentsetstextset_Status.Review:
return langui.status_review;
case Enum_Componentsetstextset_Status.Done:
return langui.status_done;
default:
return "";
}
}
export function slugify(string: string | undefined): string {
if (!string) {
return "";
}
return string
.replace(/[ÀÁÂÃÄÅàáâãä忯]/g, "a")
.replace(/[çÇ]/gu, "c")
.replace(/[ðÐ]/gu, "d")
.replace(/[ÈÉÊËéèêë]/gu, "e")
.replace(/[ÏïÎîÍíÌì]/gu, "i")
.replace(/[Ññ]/gu, "n")
.replace(/[øØœŒÕõÔôÓóÒò]/gu, "o")
.replace(/[ÜüÛûÚúÙù]/gu, "u")
.replace(/[ŸÿÝý]/gu, "y")
.toLowerCase()
.replace(/[^a-z0-9- ]/gu, "")
.trim()
.replace(/ /gu, "-");
}
export function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
export function getLocalesFromLanguages(
languages: {
language: {
data: {
attributes: {
code: string;
};
};
};
}[]
) {
return languages.map((language) => language.language.data.attributes.code);
}

View File

@ -2,89 +2,520 @@
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply box-border font-body font-medium scroll-smooth scroll-m-8;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-headers font-black;
}
a {
@apply transition-colors underline-offset-2 decoration-dotted underline decoration-dark hover:text-dark;
}
*::selection {
@apply bg-dark text-light;
}
/* SCROLLBARS STYLING */
* {
@apply [scrollbar-color:theme(colors.dark)_transparent] [scrollbar-width:thin];
}
*::-webkit-scrollbar {
@apply w-3 mobile:w-0;
}
*::-webkit-scrollbar-track {
@apply bg-light;
}
*::-webkit-scrollbar-thumb {
@apply bg-dark rounded-full border-[3px] border-solid border-light;
}
/* CHANGE PROSE DEFAULTS */
.prose,
.prose p,
.prose h1,
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6,
.prose a,
.prose strong {
@apply text-black;
}
.prose a {
@apply transition-colors underline-offset-2 decoration-dotted underline decoration-dark hover:text-dark;
}
.prose footer {
@apply border-t-[3px] border-dotted pt-6;
}
.prose footer > div {
@apply my-2 px-6 py-4 rounded-xl;
}
.prose footer > div:target {
@apply bg-mid shadow-inner-sm shadow-shade;
}
.prose li::marker {
@apply text-dark;
}
.prose blockquote {
@apply border-l-dark
}
* {
@apply box-border font-body font-medium scroll-smooth scroll-m-8;
}
@layer components {
.texture-paper-dots {
@apply [background-image:var(--theme-texture-dots)] [background-blend-mode:var(--theme-texture-dots-blend)] bg-local bg-[length:10cm];
h1,
h2,
h3,
h4,
h5,
h6 {
@apply font-headers font-black;
}
a {
@apply transition-colors underline-offset-2 decoration-dotted underline decoration-dark hover:text-dark cursor-pointer;
}
*::selection {
@apply bg-dark text-light;
}
mark {
@apply bg-mid px-2;
}
/* SCROLLBARS STYLING */
* {
@apply [scrollbar-color:theme(colors.dark)_transparent] [scrollbar-width:thin];
}
*::-webkit-scrollbar {
@apply w-3 mobile:w-0;
}
*::-webkit-scrollbar-track {
@apply bg-light;
}
*::-webkit-scrollbar-thumb {
@apply bg-dark rounded-full border-[3px] border-solid border-light;
}
/* CHANGE FORMATTED DEFAULTS */
.formatted h1,
.formatted h2,
.formatted h3,
.formatted h4,
.formatted h5,
.formatted h6 {
@apply text-center flex gap-3 justify-center;
}
.formatted h1 {
@apply text-4xl my-16;
}
.formatted h1 + h2 {
@apply -mt-10;
}
.formatted h2 {
@apply text-3xl my-12;
}
.formatted h2 + h3 {
@apply -mt-8;
}
.formatted h3 {
@apply text-2xl my-8;
}
.formatted h3 + h4 {
@apply -mt-6;
}
.formatted h4 {
@apply text-xl my-6;
}
.formatted h5 {
@apply text-lg my-4;
}
.formatted p,
.formatted strong {
@apply my-2 text-justify;
}
.formatted strong {
@apply font-black;
}
.formatted footer {
@apply border-t-[3px] border-dotted pt-6;
}
.formatted footer > div {
@apply my-2 px-6 py-4 rounded-xl;
}
.formatted footer > div:target {
@apply bg-mid shadow-inner-sm shadow-shade;
}
.formatted li::marker {
@apply text-dark;
}
.formatted blockquote {
@apply border-l-dark;
}
.formatted ul {
@apply list-disc pl-4;
}
.formatted ol {
@apply list-decimal pl-4;
}
.formatted blockquote {
@apply border-2 border-mid rounded-lg p-5 text-center my-8;
}
.formatted blockquote cite {
@apply text-dark block;
}
/* INPUT */
input,
textarea {
@apply rounded-full p-2 text-center bg-light outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] text-dark hover:bg-mid transition-all placeholder:text-dark placeholder:opacity-60;
}
input::placeholder {
@apply text-dark opacity-60;
}
input:focus-visible,
textarea:focus-within {
@apply outline-none bg-mid shadow-inner-sm shadow-shade;
}
textarea {
@apply rounded-2xl text-left p-6;
}
input[type="submit"] {
@apply grid place-content-center place-items-center border-[1px] border-dark text-dark rounded-full px-4 pt-[0.4rem] pb-[0.5rem] transition-all cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black;
}
.texture-paper-dots {
@apply [background-image:var(--theme-texture-dots)] [background-blend-mode:var(--theme-texture-dots-blend)] bg-local bg-[length:10cm];
}
/* TIPPY */
.tippy-box[data-animation="fade"][data-state="hidden"] {
@apply opacity-0;
}
[data-tippy-root] {
max-width: calc(100vw - 10px);
}
.tippy-box {
@apply relative bg-light drop-shadow-shade-xl rounded-lg transition-[transform,_visibility,_opacity];
}
.tippy-box[data-placement^="top"] > .tippy-arrow {
@apply bottom-0;
}
.tippy-box[data-placement^="top"] > .tippy-arrow:before {
bottom: -7px;
left: 0;
border-width: 8px 8px 0;
border-top-color: initial;
transform-origin: center top;
}
.tippy-box[data-placement^="bottom"] > .tippy-arrow {
top: 0;
}
.tippy-box[data-placement^="bottom"] > .tippy-arrow:before {
top: -7px;
left: 0;
border-width: 0 8px 8px;
border-bottom-color: initial;
transform-origin: center bottom;
}
.tippy-box[data-placement^="left"] > .tippy-arrow {
right: 0;
}
.tippy-box[data-placement^="left"] > .tippy-arrow:before {
border-width: 8px 0 8px 8px;
border-left-color: initial;
right: -7px;
transform-origin: center left;
}
.tippy-box[data-placement^="right"] > .tippy-arrow {
left: 0;
}
.tippy-box[data-placement^="right"] > .tippy-arrow:before {
left: -7px;
border-width: 8px 8px 8px 0;
border-right-color: initial;
transform-origin: center right;
}
.tippy-box[data-inertia][data-state="visible"] {
transition-timing-function: cubic-bezier(0.54, 1.5, 0.38, 1.11);
}
.tippy-arrow {
@apply h-4 w-4 text-light;
}
.tippy-arrow:before {
content: "";
position: absolute;
border-color: transparent;
border-style: solid;
}
.tippy-content {
@apply relative px-6 py-4 z-10;
}
/* LIGHTBOX */
@keyframes closeWindow {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.ril__outer {
@apply h-full w-full touch-none outline-none bg-shade bg-opacity-50 [backdrop-filter:blur(2px)];
}
.ril__outerClosing {
opacity: 0;
}
.ril__inner {
@apply absolute inset-0;
}
.ril__image,
.ril__imagePrev,
.ril__imageNext {
@apply absolute inset-0 m-auto max-w-none touch-none;
}
.ril__image {
@apply drop-shadow-shade-2xl;
}
.ril__navButtons {
@apply absolute inset-y-0 w-5 h-8 px-10 py-8 cursor-pointer m-auto;
}
.ril__navButtons:hover {
opacity: 1;
}
.ril__navButtons:active {
opacity: 0.7;
}
.ril__navButtonPrev {
left: 0;
background: rgba(0, 0, 0, 0.2)
url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDE5LDMgLTIsLTIgLTE2LDE2IDE2LDE2IDEsLTEgLTE1LC0xNSAxNSwtMTUgeiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==")
no-repeat center;
}
.ril__navButtonNext {
right: 0;
background: rgba(0, 0, 0, 0.2)
url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDEsMyAyLC0yIDE2LDE2IC0xNiwxNiAtMSwtMSAxNSwtMTUgLTE1LC0xNSB6IiBmaWxsPSIjRkZGIi8+PC9zdmc+")
no-repeat center;
}
.ril__caption,
.ril__toolbar {
@apply bg-shade bg-opacity-50 absolute inset-x-0 flex justify-between;
}
.ril__caption {
bottom: 0;
max-height: 150px;
overflow: auto;
}
.ril__captionContent {
padding: 10px 20px;
color: #fff;
}
.ril__toolbar {
@apply top-0 h-12;
}
.ril__toolbarSide {
height: 50px;
margin: 0;
}
.ril__toolbarLeftSide {
padding-left: 20px;
padding-right: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
.ril__toolbarRightSide {
padding-left: 0;
padding-right: 20px;
flex: 0 0 auto;
}
.ril__toolbarItem {
display: inline-block;
line-height: 50px;
padding: 0;
color: #fff;
font-size: 120%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ril__toolbarItemChild {
vertical-align: middle;
}
.ril__builtinButton {
width: 40px;
height: 35px;
cursor: pointer;
border: none;
opacity: 0.7;
}
.ril__builtinButton:hover {
opacity: 1;
}
.ril__builtinButton:active {
outline: none;
}
.ril__builtinButtonDisabled {
cursor: default;
opacity: 0.5;
}
.ril__builtinButtonDisabled:hover {
opacity: 0.5;
}
.ril__closeButton {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBkPSJtIDEsMyAxLjI1LC0xLjI1IDcuNSw3LjUgNy41LC03LjUgMS4yNSwxLjI1IC03LjUsNy41IDcuNSw3LjUgLTEuMjUsMS4yNSAtNy41LC03LjUgLTcuNSw3LjUgLTEuMjUsLTEuMjUgNy41LC03LjUgLTcuNSwtNy41IHoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=")
no-repeat center;
}
.ril__zoomInButton {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+PGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTEgMTlsNi02Ii8+PHBhdGggZD0iTTkgOGg2Ii8+PHBhdGggZD0iTTEyIDV2NiIvPjwvZz48Y2lyY2xlIGN4PSIxMiIgY3k9IjgiIHI9IjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+")
no-repeat center;
}
.ril__zoomOutButton {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+PGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTEgMTlsNi02Ii8+PHBhdGggZD0iTTkgOGg2Ii8+PC9nPjxjaXJjbGUgY3g9IjEyIiBjeT0iOCIgcj0iNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=")
no-repeat center;
}
.ril__outerAnimating {
animation-name: closeWindow;
}
@keyframes pointFade {
0%,
19.999%,
100% {
opacity: 0;
}
20% {
opacity: 1;
}
}
.ril__loadingCircle {
width: 60px;
height: 60px;
position: relative;
}
.ril__loadingCirclePoint {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.ril__loadingCirclePoint::before {
content: "";
display: block;
margin: 0 auto;
width: 11%;
height: 30%;
background-color: #fff;
border-radius: 30%;
animation: pointFade 800ms infinite ease-in-out both;
}
.ril__loadingCirclePoint:nth-of-type(1) {
transform: rotate(0deg);
}
.ril__loadingCirclePoint:nth-of-type(7) {
transform: rotate(180deg);
}
.ril__loadingCirclePoint:nth-of-type(1)::before,
.ril__loadingCirclePoint:nth-of-type(7)::before {
animation-delay: -800ms;
}
.ril__loadingCirclePoint:nth-of-type(2) {
transform: rotate(30deg);
}
.ril__loadingCirclePoint:nth-of-type(8) {
transform: rotate(210deg);
}
.ril__loadingCirclePoint:nth-of-type(2)::before,
.ril__loadingCirclePoint:nth-of-type(8)::before {
animation-delay: -666ms;
}
.ril__loadingCirclePoint:nth-of-type(3) {
transform: rotate(60deg);
}
.ril__loadingCirclePoint:nth-of-type(9) {
transform: rotate(240deg);
}
.ril__loadingCirclePoint:nth-of-type(3)::before,
.ril__loadingCirclePoint:nth-of-type(9)::before {
animation-delay: -533ms;
}
.ril__loadingCirclePoint:nth-of-type(4) {
transform: rotate(90deg);
}
.ril__loadingCirclePoint:nth-of-type(10) {
transform: rotate(270deg);
}
.ril__loadingCirclePoint:nth-of-type(4)::before,
.ril__loadingCirclePoint:nth-of-type(10)::before {
animation-delay: -400ms;
}
.ril__loadingCirclePoint:nth-of-type(5) {
transform: rotate(120deg);
}
.ril__loadingCirclePoint:nth-of-type(11) {
transform: rotate(300deg);
}
.ril__loadingCirclePoint:nth-of-type(5)::before,
.ril__loadingCirclePoint:nth-of-type(11)::before {
animation-delay: -266ms;
}
.ril__loadingCirclePoint:nth-of-type(6) {
transform: rotate(150deg);
}
.ril__loadingCirclePoint:nth-of-type(12) {
transform: rotate(330deg);
}
.ril__loadingCirclePoint:nth-of-type(6)::before,
.ril__loadingCirclePoint:nth-of-type(12)::before {
animation-delay: -133ms;
}
.ril__loadingCirclePoint:nth-of-type(7) {
transform: rotate(180deg);
}
.ril__loadingCirclePoint:nth-of-type(13) {
transform: rotate(360deg);
}
.ril__loadingCirclePoint:nth-of-type(7)::before,
.ril__loadingCirclePoint:nth-of-type(13)::before {
animation-delay: 0ms;
}
.ril__loadingContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.ril__imagePrev .ril__loadingContainer,
.ril__imageNext .ril__loadingContainer {
display: none;
}
.ril__errorContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.ril__imagePrev .ril__errorContainer,
.ril__imageNext .ril__errorContainer {
display: none;
}
.ril__loadingContainer__icon {
color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

View File

@ -18,6 +18,17 @@ const breakDektop = { min: "60rem" };
const breakMobile = { max: "60rem" };
const breakThin = { max: "25rem" };
const fontStandard = {
body: "Zen Maru Gothic",
headers: "Vollkorn",
monospace: "monospace",
};
const fontDyslexic = {
body: "OpenDyslexic",
headers: "OpenDyslexic",
monospace: "monospace",
};
/* END CONFIG */
function withOpacity(variableName) {
@ -41,9 +52,11 @@ module.exports = {
black: withOpacity("--theme-color-black"),
},
fontFamily: {
body: ["Zen Maru Gothic"],
headers: ["Vollkorn"],
monospace: ["monospace"],
body: ["var(--theme-font-body)"],
headers: ["var(--theme-font-headers)"],
monospace: ["var(--theme-font-monospace)"],
openDyslexic: ["OpenDyslexic"],
zenMaruGothic: ["Zen Maru Gothic"],
},
screens: {
desktop: breakDektop,
@ -59,9 +72,6 @@ module.exports = {
},
},
plugins: [
require("@tailwindcss/typography"),
// Colored Dropshadow
plugin(function ({ addUtilities }) {
addUtilities({
".set-theme-light": {
@ -85,6 +95,21 @@ module.exports = {
});
}),
plugin(function ({ addUtilities }) {
addUtilities({
".set-theme-font-standard": {
"--theme-font-body": fontStandard.body,
"--theme-font-headers": fontStandard.headers,
"--theme-font-monospace": fontStandard.monospace,
},
".set-theme-font-dyslexic": {
"--theme-font-body": fontDyslexic.body,
"--theme-font-headers": fontDyslexic.headers,
"--theme-font-monospace": fontStandard.monospace,
},
});
}),
plugin(function ({ addVariant, e }) {
addVariant("webkit-scrollbar", ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {