React18 #23
@ -50,6 +50,7 @@ module.exports = {
 | 
			
		||||
    "max-classes-per-file": ["error", 1],
 | 
			
		||||
    // "max-depth": ["warn", 4],
 | 
			
		||||
    // "max-lines": "warn",
 | 
			
		||||
    "max-len": ["warn", { code: 100 }],
 | 
			
		||||
    // "max-lines-per-function": "warn",
 | 
			
		||||
    // "max-nested-callbacks": "warn",
 | 
			
		||||
    // "max-params": "warn",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							@ -25,6 +25,7 @@
 | 
			
		||||
#### [Front](https://github.com/Accords-Library/accords-library.com) (this repository)
 | 
			
		||||
 | 
			
		||||
- Language: [TypeScript](https://www.typescriptlang.org/)
 | 
			
		||||
- Framework: [Next.js](https://nextjs.org/) (React)
 | 
			
		||||
- Queries: [GraphQL Code Generator](https://www.graphql-code-generator.com/)
 | 
			
		||||
  - Fetch the GraphQL schema from the GraphQL back-end endpoint
 | 
			
		||||
  - Read the operations and fragments stored as graphql files in the `src/graphql` folder
 | 
			
		||||
@ -33,28 +34,34 @@
 | 
			
		||||
  - Support for Arbitrary React Components and Component Props!
 | 
			
		||||
  - Autogenerated multi-level table of content and anchor links for the different headers
 | 
			
		||||
- Styling: [Tailwind CSS](https://tailwindcss.com/)
 | 
			
		||||
  - Good typographic defaults using [Tailwind/Typography](https://tailwindcss.com/docs/typography-plugin)
 | 
			
		||||
  - Beside the theme declaration no CSS outside of Tailwind CSS
 | 
			
		||||
  - Manually added support for scrollbar styling
 | 
			
		||||
  - Support for [Material Icons](https://fonts.google.com/icons)
 | 
			
		||||
  - Support for light and dark mode with a manual switch and system's selected theme by default
 | 
			
		||||
  - Support for creating any arbitrary theming mode by swapping CSS variables
 | 
			
		||||
  - Support for many screen sizes and resolutions
 | 
			
		||||
- Framework: [Next.js](https://nextjs.org/) (React)
 | 
			
		||||
  - Multilanguage support
 | 
			
		||||
- State Management: [React Context](https://reactjs.org/docs/context.html)
 | 
			
		||||
  - Persistent app state using LocalStorage
 | 
			
		||||
- Accessibility
 | 
			
		||||
  - Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable)
 | 
			
		||||
  - Keyboard hotkeys using [react-hot-keys](https://www.npmjs.com/package/react-hot-keys)
 | 
			
		||||
  - Support for light and dark mode with a manual switch and system's selected theme by default
 | 
			
		||||
  - Fonts can be swaped to [OpenDyslexic](https://www.npmjs.com/package/@fontsource/opendyslexic)
 | 
			
		||||
- Multilingual
 | 
			
		||||
  - By default, use the browser's language as the main language
 | 
			
		||||
  - Fallback languages are used for content which are not available in the main language
 | 
			
		||||
  - Main and fallback languages can be ordered manually by the user
 | 
			
		||||
  - At the content level, the user can know which language is available
 | 
			
		||||
  - Furthermore, the user can temporary select another language then the one that was automatically selected
 | 
			
		||||
- SSG + ISR (Static Site Generation + Incremental Static Regeneration):
 | 
			
		||||
  - The website is built before running in production
 | 
			
		||||
  - Performances are great, and possibility to deploy the app using a CDN
 | 
			
		||||
  - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted.
 | 
			
		||||
  - On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
 | 
			
		||||
- SEO
 | 
			
		||||
  - Good defaults for the metadate and OpenGraph properties
 | 
			
		||||
  - Each page can provide the thumbnail, title, description to be used
 | 
			
		||||
  - Automatic generation of the sitemap using [next-sitemap](https://www.npmjs.com/package/next-sitemap)
 | 
			
		||||
- 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.
 | 
			
		||||
  - Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it
 | 
			
		||||
  - Check for completeness, conformity, and integrity
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2245
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.json
									
									
									
									
									
								
							@ -16,39 +16,39 @@
 | 
			
		||||
    "@fontsource/material-icons": "^4.5.4",
 | 
			
		||||
    "@fontsource/material-icons-rounded": "^4.5.4",
 | 
			
		||||
    "@fontsource/opendyslexic": "^4.5.4",
 | 
			
		||||
    "@fontsource/vollkorn": "^4.5.6",
 | 
			
		||||
    "@fontsource/zen-maru-gothic": "^4.5.8",
 | 
			
		||||
    "@fontsource/vollkorn": "^4.5.9",
 | 
			
		||||
    "@fontsource/zen-maru-gothic": "^4.5.11",
 | 
			
		||||
    "@tippyjs/react": "^4.2.6",
 | 
			
		||||
    "autoprefixer": "^10.4.5",
 | 
			
		||||
    "autoprefixer": "^10.4.7",
 | 
			
		||||
    "graphql-request": "^4.2.0",
 | 
			
		||||
    "markdown-to-jsx": "^7.1.7",
 | 
			
		||||
    "next": "^12.1.2",
 | 
			
		||||
    "nodemailer": "^6.7.3",
 | 
			
		||||
    "react": "17.0.2",
 | 
			
		||||
    "react-dom": "17.0.2",
 | 
			
		||||
    "react-image-lightbox": "^5.1.4",
 | 
			
		||||
    "react-swipeable": "^6.2.1",
 | 
			
		||||
    "next": "^12.1.6",
 | 
			
		||||
    "nodemailer": "^6.7.5",
 | 
			
		||||
    "react": "18.1.0",
 | 
			
		||||
    "react-dom": "18.1.0",
 | 
			
		||||
    "react-hot-keys": "^2.7.2",
 | 
			
		||||
    "react-swipeable": "^7.0.0",
 | 
			
		||||
    "turndown": "^7.1.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@graphql-codegen/cli": "^2.6.2",
 | 
			
		||||
    "@graphql-codegen/typescript": "2.4.8",
 | 
			
		||||
    "@graphql-codegen/typescript-graphql-request": "^4.4.5",
 | 
			
		||||
    "@graphql-codegen/typescript-operations": "^2.3.5",
 | 
			
		||||
    "@types/node": "17.0.25",
 | 
			
		||||
    "@graphql-codegen/typescript": "2.4.11",
 | 
			
		||||
    "@graphql-codegen/typescript-graphql-request": "^4.4.8",
 | 
			
		||||
    "@graphql-codegen/typescript-operations": "^2.4.0",
 | 
			
		||||
    "@types/node": "17.0.33",
 | 
			
		||||
    "@types/nodemailer": "^6.4.4",
 | 
			
		||||
    "@types/react": "17.0.43",
 | 
			
		||||
    "@types/react-dom": "^17.0.14",
 | 
			
		||||
    "@types/react": "18.0.9",
 | 
			
		||||
    "@types/react-dom": "^18.0.4",
 | 
			
		||||
    "@types/turndown": "^5.0.1",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.20.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.20.0",
 | 
			
		||||
    "eslint": "^8.14.0",
 | 
			
		||||
    "eslint-config-next": "12.1.5",
 | 
			
		||||
    "graphql": "^14.7.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.23.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.23.0",
 | 
			
		||||
    "eslint": "^8.15.0",
 | 
			
		||||
    "eslint-config-next": "12.1.6",
 | 
			
		||||
    "graphql": "^16.5.0",
 | 
			
		||||
    "next-sitemap": "^2.5.20",
 | 
			
		||||
    "prettier": "^2.6.2",
 | 
			
		||||
    "prettier-plugin-organize-imports": "^2.3.4",
 | 
			
		||||
    "tailwindcss": "^3.0.24",
 | 
			
		||||
    "typescript": "^4.6.3"
 | 
			
		||||
    "typescript": "^4.6.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,19 @@
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { prettyLanguage, prettySlug } from "helpers/formatters";
 | 
			
		||||
import { getOgImage, ImageQuality, OgImage } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/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,
 | 
			
		||||
  prettySlug,
 | 
			
		||||
} from "queries/helpers";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
import { useSwipeable } from "react-swipeable";
 | 
			
		||||
import { ImageQuality } from "./Img";
 | 
			
		||||
import OrderableList from "./Inputs/OrderableList";
 | 
			
		||||
import Select from "./Inputs/Select";
 | 
			
		||||
import MainPanel from "./Panels/MainPanel";
 | 
			
		||||
import Popup from "./Popup";
 | 
			
		||||
import { OrderableList } from "./Inputs/OrderableList";
 | 
			
		||||
import { Select } from "./Inputs/Select";
 | 
			
		||||
import { MainPanel } from "./Panels/MainPanel";
 | 
			
		||||
import { Popup } from "./Popup";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  subPanel?: React.ReactNode;
 | 
			
		||||
@ -29,8 +25,19 @@ interface Props extends AppStaticProps {
 | 
			
		||||
  description?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
  const { langui, currencies, languages, subPanel, contentPanel } = props;
 | 
			
		||||
export function AppLayout(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    langui,
 | 
			
		||||
    currencies,
 | 
			
		||||
    languages,
 | 
			
		||||
    subPanel,
 | 
			
		||||
    contentPanel,
 | 
			
		||||
    thumbnail,
 | 
			
		||||
    title,
 | 
			
		||||
    navTitle,
 | 
			
		||||
    description,
 | 
			
		||||
    subPanelIcon,
 | 
			
		||||
  } = props;
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const isMobile = useMediaMobile();
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
@ -39,19 +46,23 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
  const handlers = useSwipeable({
 | 
			
		||||
    onSwipedLeft: (SwipeEventData) => {
 | 
			
		||||
      if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
      if (appLayout.mainPanelOpen) {
 | 
			
		||||
        appLayout.setMainPanelOpen(false);
 | 
			
		||||
      } else if (subPanel && contentPanel) {
 | 
			
		||||
        appLayout.setSubPanelOpen(true);
 | 
			
		||||
      if (appLayout.menuGestures) {
 | 
			
		||||
        if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
        if (appLayout.mainPanelOpen) {
 | 
			
		||||
          appLayout.setMainPanelOpen(false);
 | 
			
		||||
        } else if (subPanel && contentPanel) {
 | 
			
		||||
          appLayout.setSubPanelOpen(true);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    onSwipedRight: (SwipeEventData) => {
 | 
			
		||||
      if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
      if (appLayout.subPanelOpen) {
 | 
			
		||||
        appLayout.setSubPanelOpen(false);
 | 
			
		||||
      } else {
 | 
			
		||||
        appLayout.setMainPanelOpen(true);
 | 
			
		||||
      if (appLayout.menuGestures) {
 | 
			
		||||
        if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
        if (appLayout.subPanelOpen) {
 | 
			
		||||
          appLayout.setSubPanelOpen(false);
 | 
			
		||||
        } else {
 | 
			
		||||
          appLayout.setMainPanelOpen(true);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
@ -59,8 +70,8 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
  const turnSubIntoContent = subPanel && !contentPanel;
 | 
			
		||||
 | 
			
		||||
  const titlePrefix = "Accord’s Library";
 | 
			
		||||
  const metaImage: OgImage = props.thumbnail
 | 
			
		||||
    ? getOgImage(ImageQuality.Og, props.thumbnail)
 | 
			
		||||
  const metaImage: OgImage = thumbnail
 | 
			
		||||
    ? getOgImage(ImageQuality.Og, thumbnail)
 | 
			
		||||
    : {
 | 
			
		||||
        image: "/default_og.jpg",
 | 
			
		||||
        width: 1200,
 | 
			
		||||
@ -68,9 +79,9 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
        alt: "Accord's Library Logo",
 | 
			
		||||
      };
 | 
			
		||||
  const ogTitle =
 | 
			
		||||
    props.title ?? props.navTitle ?? prettySlug(router.asPath.split("/").pop());
 | 
			
		||||
    title ?? navTitle ?? prettySlug(router.asPath.split("/").pop());
 | 
			
		||||
 | 
			
		||||
  const metaDescription = props.description ?? langui.default_description ?? "";
 | 
			
		||||
  const metaDescription = description ?? langui.default_description ?? "";
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    document.getElementsByTagName("html")[0].style.fontSize = `${
 | 
			
		||||
@ -115,7 +126,7 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
  }, [currencySelect]);
 | 
			
		||||
 | 
			
		||||
  let gridCol = "";
 | 
			
		||||
  if (props.subPanel) {
 | 
			
		||||
  if (subPanel) {
 | 
			
		||||
    if (appLayout.mainPanelReduced) {
 | 
			
		||||
      gridCol = "grid-cols-[6rem_20rem_1fr]";
 | 
			
		||||
    } else {
 | 
			
		||||
@ -140,7 +151,9 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        {...handlers}
 | 
			
		||||
        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']`}
 | 
			
		||||
        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>
 | 
			
		||||
@ -172,7 +185,8 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
        {/* Background when navbar is opened */}
 | 
			
		||||
        <div
 | 
			
		||||
          className={`[grid-area:content] mobile:z-10 absolute inset-0 transition-[backdrop-filter] duration-500 ${
 | 
			
		||||
          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 "
 | 
			
		||||
@ -201,7 +215,10 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
            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">
 | 
			
		||||
              <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>
 | 
			
		||||
@ -212,7 +229,10 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
        {/* 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
 | 
			
		||||
            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"
 | 
			
		||||
@ -225,14 +245,21 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
        {/* 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"}`}
 | 
			
		||||
          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="[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">
 | 
			
		||||
        <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={() => {
 | 
			
		||||
@ -261,8 +288,8 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
            {subPanel && !turnSubIntoContent
 | 
			
		||||
              ? appLayout.subPanelOpen
 | 
			
		||||
                ? "close"
 | 
			
		||||
                : props.subPanelIcon
 | 
			
		||||
                ? props.subPanelIcon
 | 
			
		||||
                : subPanelIcon
 | 
			
		||||
                ? subPanelIcon
 | 
			
		||||
                : "tune"
 | 
			
		||||
              : ""}
 | 
			
		||||
          </span>
 | 
			
		||||
@ -274,7 +301,10 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
        >
 | 
			
		||||
          <h2 className="text-2xl">{langui.settings}</h2>
 | 
			
		||||
 | 
			
		||||
          <div className="mt-4 grid gap-16 justify-items-center text-center desktop:grid-cols-[auto_auto]">
 | 
			
		||||
          <div
 | 
			
		||||
            className="mt-4 grid gap-16 justify-items-center
 | 
			
		||||
            text-center desktop:grid-cols-[auto_auto]"
 | 
			
		||||
          >
 | 
			
		||||
            {router.locales && (
 | 
			
		||||
              <div>
 | 
			
		||||
                <h3 className="text-xl">{langui.languages}</h3>
 | 
			
		||||
@ -295,6 +325,12 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
                            ])
 | 
			
		||||
                          )
 | 
			
		||||
                    }
 | 
			
		||||
                    insertLabels={
 | 
			
		||||
                      new Map([
 | 
			
		||||
                        [0, langui.primary_language],
 | 
			
		||||
                        [1, langui.secondary_language],
 | 
			
		||||
                      ])
 | 
			
		||||
                    }
 | 
			
		||||
                    onChange={(items) => {
 | 
			
		||||
                      const preferredLanguages = [...items].map(
 | 
			
		||||
                        ([code]) => code
 | 
			
		||||
@ -437,6 +473,7 @@ export default function AppLayout(props: Props): JSX.Element {
 | 
			
		||||
                      (event.target as HTMLInputElement).value
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                  value={appLayout.playerName}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,16 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Chip(props: Props): JSX.Element {
 | 
			
		||||
export function Chip(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      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}`}
 | 
			
		||||
      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>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function HorizontalLine(props: Props): JSX.Element {
 | 
			
		||||
export function HorizontalLine(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`h-0 w-full my-8 border-t-[3px] border-dotted border-black ${props.className}`}
 | 
			
		||||
 | 
			
		||||
@ -1,106 +1,43 @@
 | 
			
		||||
import { UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import Image, { ImageProps } from "next/image";
 | 
			
		||||
import { getAssetURL, getImgSizesByQuality, ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { ImageProps } from "next/image";
 | 
			
		||||
import { MouseEventHandler } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  image?: UploadImageFragment | string;
 | 
			
		||||
  quality?: ImageQuality;
 | 
			
		||||
  alt?: ImageProps["alt"];
 | 
			
		||||
  layout?: ImageProps["layout"];
 | 
			
		||||
  objectFit?: ImageProps["objectFit"];
 | 
			
		||||
  priority?: ImageProps["priority"];
 | 
			
		||||
  onClick?: MouseEventHandler<HTMLImageElement>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Img(props: Props): JSX.Element {
 | 
			
		||||
  if (typeof props.image === "string") {
 | 
			
		||||
export function Img(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    className,
 | 
			
		||||
    image,
 | 
			
		||||
    quality = ImageQuality.Small,
 | 
			
		||||
    alt,
 | 
			
		||||
    onClick,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  if (typeof image === "string") {
 | 
			
		||||
    return (
 | 
			
		||||
      <img className={className} src={image} alt={alt ?? ""} loading="lazy" />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (image?.width && image.height) {
 | 
			
		||||
    const imgSize = getImgSizesByQuality(image.width, image.height, quality);
 | 
			
		||||
    return (
 | 
			
		||||
      <img
 | 
			
		||||
        className={props.className}
 | 
			
		||||
        src={props.image}
 | 
			
		||||
        alt={props.alt ?? ""}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  } else if (props.image?.width && props.image.height) {
 | 
			
		||||
    const imgSize = getImgSizesByQuality(
 | 
			
		||||
      props.image.width,
 | 
			
		||||
      props.image.height,
 | 
			
		||||
      props.quality ?? ImageQuality.Small
 | 
			
		||||
    );
 | 
			
		||||
    return (
 | 
			
		||||
      <Image
 | 
			
		||||
        className={props.className}
 | 
			
		||||
        src={getAssetURL(
 | 
			
		||||
          props.image.url,
 | 
			
		||||
          props.quality ? props.quality : ImageQuality.Small
 | 
			
		||||
        )}
 | 
			
		||||
        alt={props.alt ?? props.image.alternativeText ?? ""}
 | 
			
		||||
        width={props.layout === "fill" ? undefined : imgSize.width}
 | 
			
		||||
        height={props.layout === "fill" ? undefined : imgSize.height}
 | 
			
		||||
        layout={props.layout}
 | 
			
		||||
        objectFit={props.objectFit}
 | 
			
		||||
        priority={props.priority}
 | 
			
		||||
        unoptimized
 | 
			
		||||
        className={className}
 | 
			
		||||
        src={getAssetURL(image.url, quality)}
 | 
			
		||||
        alt={alt ?? image.alternativeText ?? ""}
 | 
			
		||||
        width={imgSize.width}
 | 
			
		||||
        height={imgSize.height}
 | 
			
		||||
        loading="lazy"
 | 
			
		||||
        onClick={onClick}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return <></>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ImageQuality {
 | 
			
		||||
  Small = "small",
 | 
			
		||||
  Medium = "medium",
 | 
			
		||||
  Large = "large",
 | 
			
		||||
  Og = "og",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAssetFilename(path: string): string {
 | 
			
		||||
  let result = path.split("/");
 | 
			
		||||
  result = result[result.length - 1].split(".");
 | 
			
		||||
  result = result
 | 
			
		||||
    .splice(0, result.length - 1)
 | 
			
		||||
    .join(".")
 | 
			
		||||
    .split("_");
 | 
			
		||||
  return result[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAssetURL(url: string, quality: ImageQuality): string {
 | 
			
		||||
  let newUrl = url;
 | 
			
		||||
  newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`);
 | 
			
		||||
  newUrl = newUrl.replace(/.jpg$/u, ".webp");
 | 
			
		||||
  newUrl = newUrl.replace(/.jpeg$/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(
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  maxSize: number
 | 
			
		||||
): { width: number; height: number } {
 | 
			
		||||
  if (width > height) {
 | 
			
		||||
    if (width < maxSize) return { width: width, height: height };
 | 
			
		||||
    return { width: maxSize, height: (height / width) * maxSize };
 | 
			
		||||
  }
 | 
			
		||||
  if (height < maxSize) return { width: width, height: height };
 | 
			
		||||
  return { width: (width / height) * maxSize, height: maxSize };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getImgSizesByQuality(
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  quality: ImageQuality
 | 
			
		||||
): { width: number; height: number } {
 | 
			
		||||
  switch (quality) {
 | 
			
		||||
    case ImageQuality.Og:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 512);
 | 
			
		||||
    case ImageQuality.Small:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 512);
 | 
			
		||||
    case ImageQuality.Medium:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 1024);
 | 
			
		||||
    case ImageQuality.Large:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 2048);
 | 
			
		||||
    default:
 | 
			
		||||
      return { width: 0, height: 0 };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { MouseEventHandler } from "react";
 | 
			
		||||
 | 
			
		||||
@ -14,7 +15,7 @@ interface Props {
 | 
			
		||||
  badgeNumber?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Button(props: Props): JSX.Element {
 | 
			
		||||
export function Button(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    draggable,
 | 
			
		||||
    id,
 | 
			
		||||
@ -39,11 +40,15 @@ export default function Button(props: Props): JSX.Element {
 | 
			
		||||
      transition-all select-none hover:[--opacityBadge:0] --opacityBadge:100 ${className} ${
 | 
			
		||||
        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:text-light 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`
 | 
			
		||||
      }`}
 | 
			
		||||
    >
 | 
			
		||||
      {badgeNumber && (
 | 
			
		||||
        <div className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full">
 | 
			
		||||
        <div
 | 
			
		||||
          className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center
 | 
			
		||||
          absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full"
 | 
			
		||||
        >
 | 
			
		||||
          {badgeNumber}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettyLanguage } from "queries/helpers";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { prettyLanguage } from "helpers/formatters";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import ToolTip from "../ToolTip";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import { ToolTip } from "../ToolTip";
 | 
			
		||||
import { Button } from "./Button";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
@ -12,7 +13,7 @@ interface Props {
 | 
			
		||||
  setLocalesIndex: Dispatch<SetStateAction<number | undefined>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function LanguageSwitcher(props: Props): JSX.Element {
 | 
			
		||||
export function LanguageSwitcher(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { locales, className, localesIndex, setLocalesIndex } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
import { arrayMove } from "queries/helpers";
 | 
			
		||||
import { arrayMove } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  items: Map<string, string>;
 | 
			
		||||
  insertLabels?: Map<number, string | null | undefined>;
 | 
			
		||||
  onChange?: (items: Map<string, string>) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function OrderableList(props: Props): JSX.Element {
 | 
			
		||||
export function OrderableList(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const [items, setItems] = useState<Map<string, string>>(props.items);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@ -24,12 +26,8 @@ export default function OrderableList(props: Props): JSX.Element {
 | 
			
		||||
    <div className="grid gap-2">
 | 
			
		||||
      {[...items].map(([key, value], index) => (
 | 
			
		||||
        <>
 | 
			
		||||
          {index === 0 ? (
 | 
			
		||||
            <p>Primary language</p>
 | 
			
		||||
          ) : index === 1 ? (
 | 
			
		||||
            <p>Secondary languages</p>
 | 
			
		||||
          ) : (
 | 
			
		||||
            ""
 | 
			
		||||
          {props.insertLabels?.get(index) && (
 | 
			
		||||
            <p>{props.insertLabels.get(index)}</p>
 | 
			
		||||
          )}
 | 
			
		||||
          <div
 | 
			
		||||
            onDragStart={(event) => {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import Button from "./Button";
 | 
			
		||||
import { Button } from "./Button";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
@ -8,7 +9,7 @@ interface Props {
 | 
			
		||||
  setPage: Dispatch<SetStateAction<number>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function PageSelector(props: Props): JSX.Element {
 | 
			
		||||
export function PageSelector(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { page, setPage, maxPage } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@ -9,7 +10,7 @@ interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Select(props: Props): JSX.Element {
 | 
			
		||||
export function Select(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const [opened, setOpened] = useState(false);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -19,7 +20,9 @@ export default function Select(props: Props): JSX.Element {
 | 
			
		||||
      } ${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 ${
 | 
			
		||||
        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"
 | 
			
		||||
        }`}
 | 
			
		||||
      >
 | 
			
		||||
@ -47,7 +50,8 @@ export default function Select(props: Props): JSX.Element {
 | 
			
		||||
          <>
 | 
			
		||||
            {index !== props.state && (
 | 
			
		||||
              <div
 | 
			
		||||
                className="bg-light hover:bg-mid transition-colors cursor-pointer p-1 last-of-type:rounded-b-[1em]"
 | 
			
		||||
                className="bg-light hover:bg-mid transition-colors
 | 
			
		||||
                cursor-pointer p-1 last-of-type:rounded-b-[1em]"
 | 
			
		||||
                key={index}
 | 
			
		||||
                id={option}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@ -6,18 +7,20 @@ interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Switch(props: Props): JSX.Element {
 | 
			
		||||
export function Switch(props: Immutable<Props>): 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"}`}
 | 
			
		||||
      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 ${
 | 
			
		||||
        className={`bg-dark aspect-square rounded-full absolute
 | 
			
		||||
        top-0 bottom-0 left-0 transition-transform ${
 | 
			
		||||
          props.state && "translate-x-[115%]"
 | 
			
		||||
        }`}
 | 
			
		||||
      ></div>
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,12 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  id?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function InsetBox(props: Props): JSX.Element {
 | 
			
		||||
export function InsetBox(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      id={props.id}
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,24 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { GetLibraryItemQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettyinlineTitle, prettySlug } from "queries/helpers";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  content: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      Exclude<
 | 
			
		||||
        GetLibraryItemQuery["libraryItems"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["data"][number]["attributes"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["contents"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  content: NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        GetLibraryItemQuery["libraryItems"]
 | 
			
		||||
      >["data"][number]["attributes"]
 | 
			
		||||
    >["contents"]
 | 
			
		||||
  >["data"][number];
 | 
			
		||||
  parentSlug: string;
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ContentLine(props: Props): JSX.Element {
 | 
			
		||||
export function ContentLine(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { content, langui, parentSlug } = props;
 | 
			
		||||
 | 
			
		||||
  const [opened, setOpened] = useState(false);
 | 
			
		||||
@ -32,15 +30,19 @@ export default function ContentLine(props: Props): JSX.Element {
 | 
			
		||||
          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]">
 | 
			
		||||
        <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?.attributes?.titles?.[0]
 | 
			
		||||
              {content.attributes.content?.data?.attributes?.translations?.[0]
 | 
			
		||||
                ? prettyinlineTitle(
 | 
			
		||||
                    content.attributes.content.data.attributes.titles[0]
 | 
			
		||||
                    content.attributes.content.data.attributes.translations[0]
 | 
			
		||||
                      ?.pre_title,
 | 
			
		||||
                    content.attributes.content.data.attributes.titles[0]?.title,
 | 
			
		||||
                    content.attributes.content.data.attributes.titles[0]
 | 
			
		||||
                    content.attributes.content.data.attributes.translations[0]
 | 
			
		||||
                      ?.title,
 | 
			
		||||
                    content.attributes.content.data.attributes.translations[0]
 | 
			
		||||
                      ?.subtitle
 | 
			
		||||
                  )
 | 
			
		||||
                : prettySlug(content.attributes.slug, props.parentSlug)}
 | 
			
		||||
 | 
			
		||||
@ -1,76 +1,55 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Img, {
 | 
			
		||||
  getAssetFilename,
 | 
			
		||||
  getAssetURL,
 | 
			
		||||
  ImageQuality,
 | 
			
		||||
} from "components/Img";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import RecorderChip from "components/RecorderChip";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Img } from "components/Img";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { RecorderChip } from "components/RecorderChip";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { GetLibraryItemScansQuery } from "graphql/generated";
 | 
			
		||||
import useSmartLanguage from "hooks/useSmartLanguage";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getStatusDescription, isInteger } from "queries/helpers";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img";
 | 
			
		||||
import { isInteger } from "helpers/numbers";
 | 
			
		||||
import { getStatusDescription } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  setLightboxOpen: Dispatch<SetStateAction<boolean>>;
 | 
			
		||||
  setLightboxImages: Dispatch<SetStateAction<string[]>>;
 | 
			
		||||
  setLightboxIndex: Dispatch<SetStateAction<number>>;
 | 
			
		||||
  scanSet: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      Exclude<
 | 
			
		||||
        Exclude<
 | 
			
		||||
          Exclude<
 | 
			
		||||
            GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
            null | undefined
 | 
			
		||||
          >["data"][number]["attributes"],
 | 
			
		||||
          null | undefined
 | 
			
		||||
        >["contents"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["data"][number]["attributes"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["scan_set"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  openLightBox: (images: string[], index?: number) => void;
 | 
			
		||||
  scanSet: NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        NonNullable<
 | 
			
		||||
          NonNullable<
 | 
			
		||||
            GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
          >["data"][number]["attributes"]
 | 
			
		||||
        >["contents"]
 | 
			
		||||
      >["data"][number]["attributes"]
 | 
			
		||||
    >["scan_set"]
 | 
			
		||||
  >;
 | 
			
		||||
  slug: string;
 | 
			
		||||
  title: string;
 | 
			
		||||
  languages: AppStaticProps["languages"];
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
  content: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      Exclude<
 | 
			
		||||
        Exclude<
 | 
			
		||||
          GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
          null | undefined
 | 
			
		||||
        >["data"][number]["attributes"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["contents"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["data"][number]["attributes"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  content: NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        NonNullable<
 | 
			
		||||
          GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
        >["data"][number]["attributes"]
 | 
			
		||||
      >["contents"]
 | 
			
		||||
    >["data"][number]["attributes"]
 | 
			
		||||
  >["content"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ScanSet(props: Props): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    setLightboxOpen,
 | 
			
		||||
    setLightboxImages,
 | 
			
		||||
    setLightboxIndex,
 | 
			
		||||
    scanSet,
 | 
			
		||||
    slug,
 | 
			
		||||
    title,
 | 
			
		||||
    languages,
 | 
			
		||||
    langui,
 | 
			
		||||
    content,
 | 
			
		||||
  } = props;
 | 
			
		||||
export function ScanSet(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { openLightBox, scanSet, slug, title, languages, langui, content } =
 | 
			
		||||
    props;
 | 
			
		||||
 | 
			
		||||
  const [selectedScan, LanguageSwitcher] = useSmartLanguage({
 | 
			
		||||
    items: scanSet,
 | 
			
		||||
    languages: languages,
 | 
			
		||||
    languageExtractor: (item) => item?.language?.data?.attributes?.code,
 | 
			
		||||
    languageExtractor: (item) => item.language?.data?.attributes?.code,
 | 
			
		||||
    transform: (item) => {
 | 
			
		||||
      item?.pages?.data.sort((a, b) => {
 | 
			
		||||
      const newItem = { ...item } as NonNullable<Props["scanSet"][number]>;
 | 
			
		||||
      newItem.pages?.data.sort((a, b) => {
 | 
			
		||||
        if (a.attributes?.url && b.attributes?.url) {
 | 
			
		||||
          let aName = getAssetFilename(a.attributes.url);
 | 
			
		||||
          let bName = getAssetFilename(b.attributes.url);
 | 
			
		||||
@ -93,7 +72,7 @@ export default function ScanSet(props: Props): JSX.Element {
 | 
			
		||||
        }
 | 
			
		||||
        return 0;
 | 
			
		||||
      });
 | 
			
		||||
      return item;
 | 
			
		||||
      return newItem;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@ -101,7 +80,10 @@ export default function ScanSet(props: Props): JSX.Element {
 | 
			
		||||
    <>
 | 
			
		||||
      {selectedScan && (
 | 
			
		||||
        <div>
 | 
			
		||||
          <div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0">
 | 
			
		||||
          <div
 | 
			
		||||
            className="flex flex-row flex-wrap place-items-center
 | 
			
		||||
          gap-6 text-base pt-10 first-of-type:pt-0"
 | 
			
		||||
          >
 | 
			
		||||
            <h2 id={slug} className="text-2xl">
 | 
			
		||||
              {title}
 | 
			
		||||
            </h2>
 | 
			
		||||
@ -198,11 +180,16 @@ export default function ScanSet(props: Props): JSX.Element {
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div 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">
 | 
			
		||||
          <div
 | 
			
		||||
            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"
 | 
			
		||||
          >
 | 
			
		||||
            {selectedScan.pages?.data.map((page, index) => (
 | 
			
		||||
              <div
 | 
			
		||||
                key={page.id}
 | 
			
		||||
                className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform"
 | 
			
		||||
                className="drop-shadow-shade-lg hover:scale-[1.02]
 | 
			
		||||
                cursor-pointer transition-transform"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  const images: string[] = [];
 | 
			
		||||
                  selectedScan.pages?.data.map((image) => {
 | 
			
		||||
@ -211,9 +198,7 @@ export default function ScanSet(props: Props): JSX.Element {
 | 
			
		||||
                        getAssetURL(image.attributes.url, ImageQuality.Large)
 | 
			
		||||
                      );
 | 
			
		||||
                  });
 | 
			
		||||
                  setLightboxOpen(true);
 | 
			
		||||
                  setLightboxImages(images);
 | 
			
		||||
                  setLightboxIndex(index);
 | 
			
		||||
                  openLightBox(images, index);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {page.attributes && (
 | 
			
		||||
 | 
			
		||||
@ -1,48 +1,37 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Img, { getAssetURL, ImageQuality } from "components/Img";
 | 
			
		||||
import RecorderChip from "components/RecorderChip";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Img } from "components/Img";
 | 
			
		||||
import { RecorderChip } from "components/RecorderChip";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import {
 | 
			
		||||
  GetLibraryItemScansQuery,
 | 
			
		||||
  UploadImageFragment,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import useSmartLanguage from "hooks/useSmartLanguage";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getStatusDescription } from "queries/helpers";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getAssetURL, ImageQuality } from "helpers/img";
 | 
			
		||||
import { getStatusDescription } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  setLightboxOpen: Dispatch<SetStateAction<boolean>>;
 | 
			
		||||
  setLightboxImages: Dispatch<SetStateAction<string[]>>;
 | 
			
		||||
  setLightboxIndex: Dispatch<SetStateAction<number>>;
 | 
			
		||||
  images: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      Exclude<
 | 
			
		||||
        GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["data"][number]["attributes"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["images"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  openLightBox: (images: string[], index?: number) => void;
 | 
			
		||||
  images: NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
      >["data"][number]["attributes"]
 | 
			
		||||
    >["images"]
 | 
			
		||||
  >;
 | 
			
		||||
  languages: AppStaticProps["languages"];
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ScanSetCover(props: Props): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    setLightboxOpen,
 | 
			
		||||
    setLightboxImages,
 | 
			
		||||
    setLightboxIndex,
 | 
			
		||||
    images,
 | 
			
		||||
    languages,
 | 
			
		||||
    langui,
 | 
			
		||||
  } = props;
 | 
			
		||||
export function ScanSetCover(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { openLightBox, images, languages, langui } = props;
 | 
			
		||||
 | 
			
		||||
  const [selectedScan, LanguageSwitcher] = useSmartLanguage({
 | 
			
		||||
    items: images,
 | 
			
		||||
    languages: languages,
 | 
			
		||||
    languageExtractor: (item) => item?.language?.data?.attributes?.code,
 | 
			
		||||
    languageExtractor: (item) => item.language?.data?.attributes?.code,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const coverImages: UploadImageFragment[] = [];
 | 
			
		||||
@ -64,7 +53,10 @@ export default function ScanSetCover(props: Props): JSX.Element {
 | 
			
		||||
      <>
 | 
			
		||||
        {selectedScan && (
 | 
			
		||||
          <div>
 | 
			
		||||
            <div className="flex flex-row flex-wrap place-items-center gap-6 text-base pt-10 first-of-type:pt-0">
 | 
			
		||||
            <div
 | 
			
		||||
              className="flex flex-row flex-wrap place-items-center
 | 
			
		||||
              gap-6 text-base pt-10 first-of-type:pt-0"
 | 
			
		||||
            >
 | 
			
		||||
              <h2 id={"cover"} className="text-2xl">
 | 
			
		||||
                {"Cover"}
 | 
			
		||||
              </h2>
 | 
			
		||||
@ -149,20 +141,23 @@ export default function ScanSetCover(props: Props): JSX.Element {
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div 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">
 | 
			
		||||
            <div
 | 
			
		||||
              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"
 | 
			
		||||
            >
 | 
			
		||||
              {coverImages.map((image, index) => (
 | 
			
		||||
                <div
 | 
			
		||||
                  key={image.url}
 | 
			
		||||
                  className="drop-shadow-shade-lg hover:scale-[1.02] cursor-pointer transition-transform"
 | 
			
		||||
                  className="drop-shadow-shade-lg hover:scale-[1.02]
 | 
			
		||||
                  cursor-pointer transition-transform"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    const imgs: string[] = [];
 | 
			
		||||
                    coverImages.map((img) => {
 | 
			
		||||
                      if (img.url)
 | 
			
		||||
                        imgs.push(getAssetURL(img.url, ImageQuality.Large));
 | 
			
		||||
                    });
 | 
			
		||||
                    setLightboxOpen(true);
 | 
			
		||||
                    setLightboxImages(imgs);
 | 
			
		||||
                    setLightboxIndex(index);
 | 
			
		||||
                    openLightBox(imgs, index);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Img image={image} quality={ImageQuality.Small} />
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,10 @@
 | 
			
		||||
import { useMediaMobile } from "hooks/useMediaQuery";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import Lightbox from "react-image-lightbox";
 | 
			
		||||
import Hotkeys from "react-hot-keys";
 | 
			
		||||
import { useSwipeable } from "react-swipeable";
 | 
			
		||||
import { Img } from "./Img";
 | 
			
		||||
import { Button } from "./Inputs/Button";
 | 
			
		||||
import { Popup } from "./Popup";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  setState:
 | 
			
		||||
@ -12,27 +16,75 @@ interface Props {
 | 
			
		||||
  setIndex: Dispatch<SetStateAction<number>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function LightBox(props: Props): JSX.Element {
 | 
			
		||||
export function LightBox(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { state, setState, images, index, setIndex } = props;
 | 
			
		||||
  const mobile = useMediaMobile();
 | 
			
		||||
 | 
			
		||||
  function handlePrevious() {
 | 
			
		||||
    if (index > 0) setIndex(index - 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleNext() {
 | 
			
		||||
    if (index < images.length - 1) setIndex(index + 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sensibilitySwipe = 0.5;
 | 
			
		||||
 | 
			
		||||
  const handlers = useSwipeable({
 | 
			
		||||
    onSwipedLeft: (SwipeEventData) => {
 | 
			
		||||
      if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
      handleNext();
 | 
			
		||||
    },
 | 
			
		||||
    onSwipedRight: (SwipeEventData) => {
 | 
			
		||||
      if (SwipeEventData.velocity < sensibilitySwipe) return;
 | 
			
		||||
      handlePrevious();
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {state && (
 | 
			
		||||
        <Lightbox
 | 
			
		||||
          reactModalProps={{
 | 
			
		||||
            parentSelector: () => document.getElementById("MyAppLayout"),
 | 
			
		||||
        <Hotkeys
 | 
			
		||||
          keyName="left,right"
 | 
			
		||||
          allowRepeat
 | 
			
		||||
          onKeyDown={(keyName) => {
 | 
			
		||||
            if (keyName === "left") {
 | 
			
		||||
              handlePrevious();
 | 
			
		||||
            } else {
 | 
			
		||||
              handleNext();
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
          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}
 | 
			
		||||
        />
 | 
			
		||||
        >
 | 
			
		||||
          <Popup setState={setState} state={state} padding={false} fillViewport>
 | 
			
		||||
            <div
 | 
			
		||||
              {...handlers}
 | 
			
		||||
              className={`grid grid-cols-[4em,1fr,4em] mobile:grid-cols-2
 | 
			
		||||
              [grid-template-areas:"left_image_right"]
 | 
			
		||||
              mobile:[grid-template-areas:"image_image""left_right"]
 | 
			
		||||
              place-items-center first-letter:gap-4 w-full h-full overflow-hidden`}
 | 
			
		||||
            >
 | 
			
		||||
              <div className="[grid-area:left]">
 | 
			
		||||
                {index > 0 && (
 | 
			
		||||
                  <Button onClick={handlePrevious}>
 | 
			
		||||
                    <span className="material-icons">chevron_left</span>
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <Img
 | 
			
		||||
                className="max-h-full [grid-area:image]"
 | 
			
		||||
                image={images[index]}
 | 
			
		||||
              />
 | 
			
		||||
 | 
			
		||||
              <div className="[grid-area:right]">
 | 
			
		||||
                {index < images.length - 1 && (
 | 
			
		||||
                  <Button onClick={handleNext}>
 | 
			
		||||
                    <span className="material-icons">chevron_right</span>
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Popup>
 | 
			
		||||
        </Hotkeys>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
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 { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
import { Img } from "components/Img";
 | 
			
		||||
import { InsetBox } from "components/InsetBox";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { slugify } from "helpers/formatters";
 | 
			
		||||
import { getAssetURL, ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useLightBox } from "hooks/useLightBox";
 | 
			
		||||
import Markdown from "markdown-to-jsx";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { slugify } from "queries/helpers";
 | 
			
		||||
import React, { useState } from "react";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import ReactDOMServer from "react-dom/server";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@ -15,26 +17,18 @@ interface Props {
 | 
			
		||||
  text: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Markdawn(props: Props): JSX.Element {
 | 
			
		||||
export function Markdawn(props: Immutable<Props>): 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);
 | 
			
		||||
  const [openLightBox, LightBox] = useLightBox();
 | 
			
		||||
 | 
			
		||||
  if (text) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <LightBox
 | 
			
		||||
          state={lightboxOpen}
 | 
			
		||||
          setState={setLightboxOpen}
 | 
			
		||||
          images={lightboxImages}
 | 
			
		||||
          index={lightboxIndex}
 | 
			
		||||
          setIndex={setLightboxIndex}
 | 
			
		||||
        />
 | 
			
		||||
        <LightBox />
 | 
			
		||||
        <Markdown
 | 
			
		||||
          className={`formatted ${props.className}`}
 | 
			
		||||
          options={{
 | 
			
		||||
@ -253,13 +247,11 @@ export default function Markdawn(props: Props): JSX.Element {
 | 
			
		||||
                  <div
 | 
			
		||||
                    className="my-8 cursor-pointer place-content-center grid"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setLightboxOpen(true);
 | 
			
		||||
                      setLightboxImages([
 | 
			
		||||
                      openLightBox([
 | 
			
		||||
                        compProps.src.startsWith("/uploads/")
 | 
			
		||||
                          ? getAssetURL(compProps.src, ImageQuality.Large)
 | 
			
		||||
                          : compProps.src,
 | 
			
		||||
                      ]);
 | 
			
		||||
                      setLightboxIndex(0);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Img
 | 
			
		||||
@ -268,8 +260,6 @@ export default function Markdawn(props: Props): JSX.Element {
 | 
			
		||||
                          ? getAssetURL(compProps.src, ImageQuality.Small)
 | 
			
		||||
                          : compProps.src
 | 
			
		||||
                      }
 | 
			
		||||
                      layout="fill"
 | 
			
		||||
                      objectFit="contain"
 | 
			
		||||
                      quality={ImageQuality.Medium}
 | 
			
		||||
                    ></Img>
 | 
			
		||||
                  </div>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { slugify } from "helpers/formatters";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { slugify } from "queries/helpers";
 | 
			
		||||
import { preprocessMarkDawn } from "./Markdawn";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@ -7,7 +8,7 @@ interface Props {
 | 
			
		||||
  title?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function TOCComponent(props: Props): JSX.Element {
 | 
			
		||||
export function TOC(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { text, title } = props;
 | 
			
		||||
  const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
@ -28,7 +29,7 @@ export default function TOCComponent(props: Props): JSX.Element {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LevelProps {
 | 
			
		||||
  tocchildren: TOC[];
 | 
			
		||||
  tocchildren: TOCInterface[];
 | 
			
		||||
  parentNumbering: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -60,14 +61,14 @@ function TOCLevel(props: LevelProps): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TOC {
 | 
			
		||||
interface TOCInterface {
 | 
			
		||||
  title: string;
 | 
			
		||||
  slug: string;
 | 
			
		||||
  children: TOC[];
 | 
			
		||||
  children: TOCInterface[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getTocFromMarkdawn(text: string, title?: string): TOC {
 | 
			
		||||
  const toc: TOC = {
 | 
			
		||||
export function getTocFromMarkdawn(text: string, title?: string): TOCInterface {
 | 
			
		||||
  const toc: TOCInterface = {
 | 
			
		||||
    title: title ?? "Return to top",
 | 
			
		||||
    slug: slugify(title),
 | 
			
		||||
    children: [],
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { MouseEventHandler } from "react";
 | 
			
		||||
 | 
			
		||||
@ -12,15 +13,19 @@ interface Props {
 | 
			
		||||
  onClick?: MouseEventHandler<HTMLDivElement>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function NavOption(props: Props): JSX.Element {
 | 
			
		||||
export function NavOption(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
  const isActive = router.asPath.startsWith(props.url);
 | 
			
		||||
  const divActive = "bg-mid shadow-inner-sm shadow-shade";
 | 
			
		||||
 | 
			
		||||
  const border =
 | 
			
		||||
    "outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent]";
 | 
			
		||||
  const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner hover:active:shadow-shade transition-all ${
 | 
			
		||||
    props.border ? border : ""
 | 
			
		||||
  } ${isActive ? divActive : ""}`;
 | 
			
		||||
 | 
			
		||||
  const divCommon = `gap-x-5 w-full rounded-2xl cursor-pointer p-4 hover:bg-mid
 | 
			
		||||
  hover:shadow-inner-sm hover:shadow-shade hover:active:shadow-inner
 | 
			
		||||
  hover:active:shadow-shade transition-all ${props.border ? border : ""} ${
 | 
			
		||||
    isActive ? divActive : ""
 | 
			
		||||
  }`;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolTip
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import HorizontalLine from "components/HorizontalLine";
 | 
			
		||||
import { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  icon?: string;
 | 
			
		||||
@ -6,7 +7,7 @@ interface Props {
 | 
			
		||||
  description?: string | null | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function PanelHeader(props: Props): JSX.Element {
 | 
			
		||||
export function PanelHeader(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className="w-full grid place-items-center">
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import HorizontalLine from "components/HorizontalLine";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  href: string;
 | 
			
		||||
@ -18,7 +19,7 @@ export enum ReturnButtonType {
 | 
			
		||||
  both = "both",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ReturnButton(props: Props): JSX.Element {
 | 
			
		||||
export function ReturnButton(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  autoformat?: boolean;
 | 
			
		||||
@ -9,13 +11,13 @@ export enum ContentPanelWidthSizes {
 | 
			
		||||
  large = "large",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ContentPanel(props: Props): JSX.Element {
 | 
			
		||||
export function ContentPanel(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const width = props.width ? props.width : ContentPanelWidthSizes.default;
 | 
			
		||||
  const widthCSS =
 | 
			
		||||
    width === ContentPanelWidthSizes.default ? "max-w-2xl" : "w-full";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={`grid pt-10 pb-20 px-6 desktop:py-20 desktop:px-10`}>
 | 
			
		||||
    <div className={`grid pt-10 pb-20 px-4 desktop:py-20 desktop:px-10`}>
 | 
			
		||||
      <main
 | 
			
		||||
        className={`${
 | 
			
		||||
          props.autoformat && "formatted"
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,27 @@
 | 
			
		||||
import HorizontalLine from "components/HorizontalLine";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useMediaDesktop } from "hooks/useMediaQuery";
 | 
			
		||||
import Markdown from "markdown-to-jsx";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function MainPanel(props: Props): JSX.Element {
 | 
			
		||||
export function MainPanel(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const isDesktop = useMediaDesktop();
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={`flex flex-col justify-center content-start gap-y-2 justify-items-center text-center p-8 ${
 | 
			
		||||
      className={`flex flex-col justify-center content-start
 | 
			
		||||
      gap-y-2 justify-items-center text-center p-8 ${
 | 
			
		||||
        appLayout.mainPanelReduced && isDesktop && "px-4"
 | 
			
		||||
      }`}
 | 
			
		||||
    >
 | 
			
		||||
@ -44,7 +46,9 @@ export default function MainPanel(props: Props): JSX.Element {
 | 
			
		||||
              onClick={() => appLayout.setMainPanelOpen(false)}
 | 
			
		||||
              className={`${
 | 
			
		||||
                appLayout.mainPanelReduced && isDesktop ? "w-12" : "w-1/2"
 | 
			
		||||
              } aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black hover:bg-dark mb-4`}
 | 
			
		||||
              } aspect-square cursor-pointer transition-colors [mask:url('/icons/accords.svg')]
 | 
			
		||||
              ![mask-size:contain] ![mask-repeat:no-repeat]
 | 
			
		||||
              ![mask-position:center] bg-black hover:bg-dark mb-4`}
 | 
			
		||||
            ></div>
 | 
			
		||||
          </Link>
 | 
			
		||||
 | 
			
		||||
@ -68,22 +72,11 @@ export default function MainPanel(props: Props): JSX.Element {
 | 
			
		||||
              disabled={!appLayout.mainPanelReduced}
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                className={
 | 
			
		||||
                  appLayout.mainPanelReduced && isDesktop
 | 
			
		||||
                    ? ""
 | 
			
		||||
                    : "!py-0.5 !px-2.5"
 | 
			
		||||
                }
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  appLayout.setConfigPanelOpen(true);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <span
 | 
			
		||||
                  className={`material-icons ${
 | 
			
		||||
                    !(appLayout.mainPanelReduced && isDesktop) && "!text-sm"
 | 
			
		||||
                  } `}
 | 
			
		||||
                >
 | 
			
		||||
                  settings
 | 
			
		||||
                </span>
 | 
			
		||||
                <span className={"material-icons"}>settings</span>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </ToolTip>
 | 
			
		||||
 | 
			
		||||
@ -218,10 +211,22 @@ export default function MainPanel(props: Props): JSX.Element {
 | 
			
		||||
          className="transition-[filter] colorize-black hover:colorize-dark"
 | 
			
		||||
          href="https://creativecommons.org/licenses/by-sa/4.0/"
 | 
			
		||||
        >
 | 
			
		||||
          <div className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1 hover:[--theme-color-black:var(--theme-color-dark)]">
 | 
			
		||||
            <div className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" />
 | 
			
		||||
            <div className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" />
 | 
			
		||||
            <div className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black" />
 | 
			
		||||
          <div
 | 
			
		||||
            className="mt-4 mb-8 grid grid-flow-col place-content-center gap-1
 | 
			
		||||
            hover:[--theme-color-black:var(--theme-color-dark)]"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              className="w-6 aspect-square [mask:url('/icons/creative-commons-brands.svg')]
 | 
			
		||||
              ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              className="w-6 aspect-square [mask:url('/icons/creative-commons-by-brands.svg')]
 | 
			
		||||
              ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
 | 
			
		||||
            />
 | 
			
		||||
            <div
 | 
			
		||||
              className="w-6 aspect-square [mask:url('/icons/creative-commons-sa-brands.svg')]
 | 
			
		||||
              ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] bg-black"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </a>
 | 
			
		||||
        <p>
 | 
			
		||||
@ -232,14 +237,18 @@ export default function MainPanel(props: Props): JSX.Element {
 | 
			
		||||
        <div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
 | 
			
		||||
          <a
 | 
			
		||||
            aria-label="Browse our GitHub repository, which include this website source code"
 | 
			
		||||
            className="transition-colors [mask:url('/icons/github-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark"
 | 
			
		||||
            className="transition-colors [mask:url('/icons/github-brands.svg')]
 | 
			
		||||
            ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center]
 | 
			
		||||
            w-10 aspect-square bg-black hover:bg-dark"
 | 
			
		||||
            href="https://github.com/Accords-Library"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noopener noreferrer"
 | 
			
		||||
          ></a>
 | 
			
		||||
          <a
 | 
			
		||||
            aria-label="Join our Discord server!"
 | 
			
		||||
            className="transition-colors [mask:url('/icons/discord-brands.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center] w-10 aspect-square bg-black hover:bg-dark"
 | 
			
		||||
            className="transition-colors [mask:url('/icons/discord-brands.svg')]
 | 
			
		||||
            ![mask-size:contain] ![mask-repeat:no-repeat] ![mask-position:center]
 | 
			
		||||
            w-10 aspect-square bg-black hover:bg-dark"
 | 
			
		||||
            href="/discord"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noopener noreferrer"
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function SubPanel(props: Props): JSX.Element {
 | 
			
		||||
export function SubPanel(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="grid pt-10 pb-20 px-6 desktop:py-8 desktop:px-10 gap-y-2 text-center">
 | 
			
		||||
      {props.children}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,7 @@
 | 
			
		||||
import { Dispatch, SetStateAction } from "react";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Dispatch, SetStateAction, useEffect } from "react";
 | 
			
		||||
import Hotkeys from "react-hot-keys";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  setState:
 | 
			
		||||
@ -8,42 +11,65 @@ interface Props {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  fillViewport?: boolean;
 | 
			
		||||
  hideBackground?: boolean;
 | 
			
		||||
  padding?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Popup(props: Props): JSX.Element {
 | 
			
		||||
export function Popup(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    setState,
 | 
			
		||||
    state,
 | 
			
		||||
    children,
 | 
			
		||||
    fillViewport,
 | 
			
		||||
    hideBackground,
 | 
			
		||||
    padding = true,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    appLayout.setMenuGestures(!state);
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [state]);
 | 
			
		||||
 | 
			
		||||
  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"
 | 
			
		||||
      }`}
 | 
			
		||||
    <Hotkeys
 | 
			
		||||
      keyName="escape"
 | 
			
		||||
      allowRepeat
 | 
			
		||||
      onKeyDown={() => {
 | 
			
		||||
        setState(false);
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <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 max-h-[80vh] overflow-y-auto mobile:w-[85vw]"
 | 
			
		||||
        } ${
 | 
			
		||||
          props.hideBackground
 | 
			
		||||
            ? ""
 | 
			
		||||
            : "bg-light rounded-lg shadow-2xl shadow-shade"
 | 
			
		||||
        }`}
 | 
			
		||||
        className={`fixed inset-0 z-50 grid place-content-center
 | 
			
		||||
      transition-[backdrop-filter] duration-500 ${
 | 
			
		||||
        state ? "[backdrop-filter:blur(2px)]" : "pointer-events-none touch-none"
 | 
			
		||||
      }`}
 | 
			
		||||
      >
 | 
			
		||||
        {props.children}
 | 
			
		||||
        <div
 | 
			
		||||
          className={`fixed bg-shade inset-0 transition-all duration-500 ${
 | 
			
		||||
            state ? "bg-opacity-50" : "bg-opacity-0"
 | 
			
		||||
          }`}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setState(false);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <div
 | 
			
		||||
          className={`${
 | 
			
		||||
            padding && "p-10 mobile:p-6"
 | 
			
		||||
          } grid gap-4 place-items-center transition-transform ${
 | 
			
		||||
            state ? "scale-100" : "scale-0"
 | 
			
		||||
          } ${
 | 
			
		||||
            fillViewport
 | 
			
		||||
              ? "absolute inset-10"
 | 
			
		||||
              : "relative max-h-[80vh] overflow-y-auto mobile:w-[85vw]"
 | 
			
		||||
          } ${
 | 
			
		||||
            hideBackground ? "" : "bg-light rounded-lg shadow-2xl shadow-shade"
 | 
			
		||||
          }`}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    </Hotkeys>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,22 @@
 | 
			
		||||
import { GetPostQuery } from "graphql/generated";
 | 
			
		||||
import useSmartLanguage from "hooks/useSmartLanguage";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getStatusDescription, prettySlug } from "queries/helpers";
 | 
			
		||||
import AppLayout from "./AppLayout";
 | 
			
		||||
import Chip from "./Chip";
 | 
			
		||||
import HorizontalLine from "./HorizontalLine";
 | 
			
		||||
import Markdawn from "./Markdown/Markdawn";
 | 
			
		||||
import TOC from "./Markdown/TOC";
 | 
			
		||||
import ReturnButton, { ReturnButtonType } from "./PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel from "./Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "./Panels/SubPanel";
 | 
			
		||||
import RecorderChip from "./RecorderChip";
 | 
			
		||||
import ThumbnailHeader from "./ThumbnailHeader";
 | 
			
		||||
import ToolTip from "./ToolTip";
 | 
			
		||||
 | 
			
		||||
export type Post = Exclude<
 | 
			
		||||
  Exclude<
 | 
			
		||||
    GetPostQuery["posts"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number]["attributes"],
 | 
			
		||||
  null | undefined
 | 
			
		||||
>;
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { prettySlug } from "helpers/formatters";
 | 
			
		||||
import { getStatusDescription } from "helpers/others";
 | 
			
		||||
import { Immutable, PostWithTranslations } from "helpers/types";
 | 
			
		||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
 | 
			
		||||
import { AppLayout } from "./AppLayout";
 | 
			
		||||
import { Chip } from "./Chip";
 | 
			
		||||
import { HorizontalLine } from "./HorizontalLine";
 | 
			
		||||
import { Markdawn } from "./Markdown/Markdawn";
 | 
			
		||||
import { TOC } from "./Markdown/TOC";
 | 
			
		||||
import { ReturnButton, ReturnButtonType } from "./PanelComponents/ReturnButton";
 | 
			
		||||
import { ContentPanel } from "./Panels/ContentPanel";
 | 
			
		||||
import { SubPanel } from "./Panels/SubPanel";
 | 
			
		||||
import { RecorderChip } from "./RecorderChip";
 | 
			
		||||
import { ThumbnailHeader } from "./ThumbnailHeader";
 | 
			
		||||
import { ToolTip } from "./ToolTip";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  post: Post;
 | 
			
		||||
  post: PostWithTranslations;
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
  languages: AppStaticProps["languages"];
 | 
			
		||||
  currencies: AppStaticProps["currencies"];
 | 
			
		||||
@ -38,7 +31,7 @@ interface Props {
 | 
			
		||||
  appendBody?: JSX.Element;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function PostPage(props: Props): JSX.Element {
 | 
			
		||||
export function PostPage(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    post,
 | 
			
		||||
    langui,
 | 
			
		||||
 | 
			
		||||
@ -4,16 +4,18 @@ import {
 | 
			
		||||
  PricePickerFragment,
 | 
			
		||||
  UploadImageFragment,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import {
 | 
			
		||||
  prettyDate,
 | 
			
		||||
  prettyDuration,
 | 
			
		||||
  prettyPrice,
 | 
			
		||||
  prettyShortenNumber,
 | 
			
		||||
} from "queries/helpers";
 | 
			
		||||
import Chip from "./Chip";
 | 
			
		||||
import Img, { ImageQuality } from "./Img";
 | 
			
		||||
} from "helpers/formatters";
 | 
			
		||||
import { ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import { Chip } from "./Chip";
 | 
			
		||||
import { Img } from "./Img";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  thumbnail?: UploadImageFragment | string | null | undefined;
 | 
			
		||||
@ -26,6 +28,7 @@ interface Props {
 | 
			
		||||
  topChips?: string[];
 | 
			
		||||
  bottomChips?: string[];
 | 
			
		||||
  keepInfoVisible?: boolean;
 | 
			
		||||
  stackNumber?: number;
 | 
			
		||||
  metadata?: {
 | 
			
		||||
    currencies?: AppStaticProps["currencies"];
 | 
			
		||||
    release_date?: DatePickerFragment | null;
 | 
			
		||||
@ -42,7 +45,7 @@ interface Props {
 | 
			
		||||
    | { __typename: "anotherHoverlayName" };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
export function PreviewCard(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    href,
 | 
			
		||||
    thumbnail,
 | 
			
		||||
@ -50,6 +53,7 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
    title,
 | 
			
		||||
    subtitle,
 | 
			
		||||
    description,
 | 
			
		||||
    stackNumber = 0,
 | 
			
		||||
    topChips,
 | 
			
		||||
    bottomChips,
 | 
			
		||||
    keepInfoVisible,
 | 
			
		||||
@ -110,8 +114,41 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
        className="drop-shadow-shade-xl cursor-pointer grid items-end
 | 
			
		||||
        fine:[--cover-opacity:0] hover:[--cover-opacity:1] hover:scale-[1.02]
 | 
			
		||||
        [--bg-opacity:0] hover:[--bg-opacity:0.5] [--play-opacity:0]
 | 
			
		||||
        hover:[--play-opacity:100] transition-transform"
 | 
			
		||||
        hover:[--play-opacity:100] transition-transform
 | 
			
		||||
        [--stacked-top:0] hover:[--stacked-top:1]"
 | 
			
		||||
      >
 | 
			
		||||
        {stackNumber > 0 && (
 | 
			
		||||
          <>
 | 
			
		||||
            <div
 | 
			
		||||
              className="bg-light rounded-md overflow-hidden absolute transition-[top_transform]
 | 
			
		||||
              inset-0 -top-[var(--stacked-top)*2.1rem] brightness-[0.8] sepia-[0.5]
 | 
			
		||||
              scale-[calc(1-0.15*var(--stacked-top))]"
 | 
			
		||||
            >
 | 
			
		||||
              {thumbnail && (
 | 
			
		||||
                <Img
 | 
			
		||||
                  className="opacity-30 "
 | 
			
		||||
                  image={thumbnail}
 | 
			
		||||
                  quality={ImageQuality.Medium}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div
 | 
			
		||||
              className="bg-light rounded-md overflow-hidden absolute transition-[top_transform]
 | 
			
		||||
              -top-[var(--stacked-top)*1rem] inset-0 brightness-[0.9] sepia-[0.2]
 | 
			
		||||
              scale-[calc(1-0.06*var(--stacked-top))]"
 | 
			
		||||
            >
 | 
			
		||||
              {thumbnail && (
 | 
			
		||||
                <Img
 | 
			
		||||
                  className="opacity-70"
 | 
			
		||||
                  image={thumbnail}
 | 
			
		||||
                  quality={ImageQuality.Medium}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {thumbnail ? (
 | 
			
		||||
          <div className="relative">
 | 
			
		||||
            <Img
 | 
			
		||||
@ -123,6 +160,14 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
              image={thumbnail}
 | 
			
		||||
              quality={ImageQuality.Medium}
 | 
			
		||||
            />
 | 
			
		||||
            {stackNumber > 0 && (
 | 
			
		||||
              <div
 | 
			
		||||
                className="absolute right-2 top-2 text-light bg-black
 | 
			
		||||
                  bg-opacity-60 px-2 rounded-full"
 | 
			
		||||
              >
 | 
			
		||||
                {stackNumber}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
            {hoverlay && hoverlay.__typename === "Video" && (
 | 
			
		||||
              <>
 | 
			
		||||
                <div
 | 
			
		||||
@ -149,18 +194,26 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div
 | 
			
		||||
            style={{ aspectRatio: thumbnailAspectRatio }}
 | 
			
		||||
            className={`w-full bg-light ${
 | 
			
		||||
            className={`w-full bg-light relative ${
 | 
			
		||||
              keepInfoVisible
 | 
			
		||||
                ? "rounded-t-md"
 | 
			
		||||
                : "rounded-md coarse:rounded-b-none"
 | 
			
		||||
            }`}
 | 
			
		||||
          ></div>
 | 
			
		||||
          >
 | 
			
		||||
            {stackNumber > 0 && (
 | 
			
		||||
              <div
 | 
			
		||||
                className="absolute right-2 top-2 text-light bg-black
 | 
			
		||||
                bg-opacity-60 px-2 rounded-full"
 | 
			
		||||
              >
 | 
			
		||||
                {stackNumber}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
        <div
 | 
			
		||||
          className={`linearbg-obi ${
 | 
			
		||||
            keepInfoVisible
 | 
			
		||||
              ? "-mt-[0.3333em]"
 | 
			
		||||
              : `fine:drop-shadow-shade-lg fine:absolute coarse:rounded-b-md
 | 
			
		||||
            !keepInfoVisible &&
 | 
			
		||||
            `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`}
 | 
			
		||||
        >
 | 
			
		||||
@ -173,11 +226,15 @@ export default function ThumbnailPreview(props: Props): JSX.Element {
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          <div className="my-1">
 | 
			
		||||
            {pre_title && <p className="leading-none mb-1">{pre_title}</p>}
 | 
			
		||||
            {title && (
 | 
			
		||||
              <p className="font-headers text-lg leading-none">{title}</p>
 | 
			
		||||
            {pre_title && (
 | 
			
		||||
              <p className="leading-none mb-1 break-words">{pre_title}</p>
 | 
			
		||||
            )}
 | 
			
		||||
            {subtitle && <p className="leading-none">{subtitle}</p>}
 | 
			
		||||
            {title && (
 | 
			
		||||
              <p className="font-headers text-lg leading-none break-words">
 | 
			
		||||
                {title}
 | 
			
		||||
              </p>
 | 
			
		||||
            )}
 | 
			
		||||
            {subtitle && <p className="leading-none break-words">{subtitle}</p>}
 | 
			
		||||
          </div>
 | 
			
		||||
          {description && <p>{description}</p>}
 | 
			
		||||
          {bottomChips && bottomChips.length > 0 && (
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
import { UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import { ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import Link from "next/link";
 | 
			
		||||
import Chip from "./Chip";
 | 
			
		||||
import Img, { ImageQuality } from "./Img";
 | 
			
		||||
import { Chip } from "./Chip";
 | 
			
		||||
import { Img } from "./Img";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  thumbnail?: UploadImageFragment | string | null | undefined;
 | 
			
		||||
@ -14,7 +16,7 @@ interface Props {
 | 
			
		||||
  bottomChips?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function PreviewLine(props: Props): JSX.Element {
 | 
			
		||||
export function PreviewLine(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    href,
 | 
			
		||||
    thumbnail,
 | 
			
		||||
@ -29,8 +31,9 @@ export default function PreviewLine(props: Props): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href={href} passHref>
 | 
			
		||||
      <div
 | 
			
		||||
        className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer hover:scale-[1.02]
 | 
			
		||||
         transition-transform flex flex-row gap-4 overflow-hidden place-items-center pr-4 w-full h-36"
 | 
			
		||||
        className="drop-shadow-shade-xl rounded-md bg-light cursor-pointer
 | 
			
		||||
        hover:scale-[1.02] transition-transform flex flex-row gap-4
 | 
			
		||||
        overflow-hidden place-items-center pr-4 w-full h-36"
 | 
			
		||||
      >
 | 
			
		||||
        {thumbnail ? (
 | 
			
		||||
          <div className="h-full aspect-[3/2]">
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,11 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { RecorderChipFragment } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import Img, { ImageQuality } from "./Img";
 | 
			
		||||
import Markdawn from "./Markdown/Markdawn";
 | 
			
		||||
import ToolTip from "./ToolTip";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { Img } from "./Img";
 | 
			
		||||
import { Markdawn } from "./Markdown/Markdawn";
 | 
			
		||||
import { ToolTip } from "./ToolTip";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
@ -11,7 +13,7 @@ interface Props {
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function RecorderChip(props: Props): JSX.Element {
 | 
			
		||||
export function RecorderChip(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { recorder, langui } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <ToolTip
 | 
			
		||||
@ -49,10 +51,7 @@ export default function RecorderChip(props: Props): JSX.Element {
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {recorder.bio?.[0] && <Markdawn text={recorder.bio[0].bio ?? ""} />}
 | 
			
		||||
 | 
			
		||||
          {/* <Button className="cursor-not-allowed">View profile</Button> */}
 | 
			
		||||
        </div>
 | 
			
		||||
      }
 | 
			
		||||
      placement="top"
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import Image from "next/image";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
@ -6,7 +7,7 @@ interface Props {
 | 
			
		||||
  className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function SVG(props: Props): JSX.Element {
 | 
			
		||||
export function SVG(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={props.className}>
 | 
			
		||||
      <Image
 | 
			
		||||
 | 
			
		||||
@ -1,36 +1,31 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Img, { ImageQuality } from "components/Img";
 | 
			
		||||
import InsetBox from "components/InsetBox";
 | 
			
		||||
import Markdawn from "components/Markdown/Markdawn";
 | 
			
		||||
import { GetContentQuery, UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettyinlineTitle, prettySlug, slugify } from "queries/helpers";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Img } from "components/Img";
 | 
			
		||||
import { InsetBox } from "components/InsetBox";
 | 
			
		||||
import { Markdawn } from "components/Markdown/Markdawn";
 | 
			
		||||
import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { prettyinlineTitle, prettySlug, slugify } from "helpers/formatters";
 | 
			
		||||
import { getAssetURL, ImageQuality } from "helpers/img";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useLightBox } from "hooks/useLightBox";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  pre_title?: string | null | undefined;
 | 
			
		||||
  title: string | null | undefined;
 | 
			
		||||
  subtitle?: string | null | undefined;
 | 
			
		||||
  description?: string | null | undefined;
 | 
			
		||||
  type?: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      GetContentQuery["contents"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["data"][number]["attributes"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  type?: NonNullable<
 | 
			
		||||
    NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
 | 
			
		||||
  >["type"];
 | 
			
		||||
  categories?: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      GetContentQuery["contents"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["data"][number]["attributes"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  categories?: NonNullable<
 | 
			
		||||
    NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
 | 
			
		||||
  >["categories"];
 | 
			
		||||
  thumbnail?: UploadImageFragment | null | undefined;
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
  languageSwitcher?: JSX.Element;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ThumbnailHeader(props: Props): JSX.Element {
 | 
			
		||||
export function ThumbnailHeader(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const {
 | 
			
		||||
    langui,
 | 
			
		||||
    pre_title,
 | 
			
		||||
@ -43,16 +38,21 @@ export default function ThumbnailHeader(props: Props): JSX.Element {
 | 
			
		||||
    languageSwitcher,
 | 
			
		||||
  } = props;
 | 
			
		||||
 | 
			
		||||
  const [openLightBox, LightBox] = useLightBox();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <LightBox />
 | 
			
		||||
      <div className="grid place-items-center gap-12 mb-12">
 | 
			
		||||
        <div className="drop-shadow-shade-lg">
 | 
			
		||||
          {thumbnail ? (
 | 
			
		||||
            <Img
 | 
			
		||||
              className=" rounded-xl"
 | 
			
		||||
              className="rounded-xl cursor-pointer"
 | 
			
		||||
              image={thumbnail}
 | 
			
		||||
              quality={ImageQuality.Medium}
 | 
			
		||||
              priority
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                openLightBox([getAssetURL(thumbnail.url, ImageQuality.Large)]);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div className="w-96 aspect-[4/3] bg-light rounded-xl"></div>
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,13 @@ import "tippy.js/animations/scale-subtle.css";
 | 
			
		||||
 | 
			
		||||
interface Props extends TippyProps {}
 | 
			
		||||
 | 
			
		||||
export default function ToolTip(props: Props): 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";
 | 
			
		||||
export function ToolTip(props: Props): JSX.Element {
 | 
			
		||||
  const newProps: Props = {
 | 
			
		||||
    delay: [150, 0],
 | 
			
		||||
    interactive: true,
 | 
			
		||||
    animation: "scale-subtle",
 | 
			
		||||
    ...props,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Tippy className={`text-[80%] ${newProps.className}`} {...newProps}>
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,20 @@
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import {
 | 
			
		||||
  Enum_Componenttranslationschronologyitem_Status,
 | 
			
		||||
  GetChronologyItemsQuery,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getStatusDescription } from "queries/helpers";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getStatusDescription } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  item: Exclude<
 | 
			
		||||
    GetChronologyItemsQuery["chronologyItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number];
 | 
			
		||||
  item: NonNullable<GetChronologyItemsQuery["chronologyItems"]>["data"][number];
 | 
			
		||||
  displayYear: boolean;
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ChronologyItemComponent(props: Props): JSX.Element {
 | 
			
		||||
export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
 | 
			
		||||
  function generateAnchor(
 | 
			
		||||
@ -71,7 +69,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
 | 
			
		||||
  if (props.item.attributes) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div
 | 
			
		||||
        className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em] py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4"
 | 
			
		||||
        className="grid place-content-start grid-rows-[auto_1fr] grid-cols-[4em]
 | 
			
		||||
        py-4 px-8 rounded-2xl target:bg-mid target:py-8 target:my-4"
 | 
			
		||||
        id={generateAnchor(
 | 
			
		||||
          props.item.attributes.year,
 | 
			
		||||
          props.item.attributes.month,
 | 
			
		||||
@ -100,7 +99,10 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
 | 
			
		||||
                    <>
 | 
			
		||||
                      {translation && (
 | 
			
		||||
                        <>
 | 
			
		||||
                          <div className="place-items-start place-content-start grid grid-flow-col gap-2">
 | 
			
		||||
                          <div
 | 
			
		||||
                            className="place-items-start
 | 
			
		||||
                            place-content-start grid grid-flow-col gap-2"
 | 
			
		||||
                          >
 | 
			
		||||
                            {translation.status !==
 | 
			
		||||
                              Enum_Componenttranslationschronologyitem_Status.Done && (
 | 
			
		||||
                              <ToolTip
 | 
			
		||||
@ -125,7 +127,8 @@ export default function ChronologyItemComponent(props: Props): JSX.Element {
 | 
			
		||||
                              className={
 | 
			
		||||
                                event.translations &&
 | 
			
		||||
                                event.translations.length > 1
 | 
			
		||||
                                  ? "before:content-['-'] before:text-dark before:inline-block before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line"
 | 
			
		||||
                                  ? `before:content-['-'] before:text-dark before:inline-block
 | 
			
		||||
                                    before:w-4 before:ml-[-1em] mt-2 whitespace-pre-line`
 | 
			
		||||
                                  : "whitespace-pre-line"
 | 
			
		||||
                              }
 | 
			
		||||
                            >
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,17 @@
 | 
			
		||||
import ChronologyItemComponent from "components/Wiki/Chronology/ChronologyItemComponent";
 | 
			
		||||
import { ChronologyItemComponent } from "components/Wiki/Chronology/ChronologyItemComponent";
 | 
			
		||||
import { GetChronologyItemsQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  year: number;
 | 
			
		||||
  items: Exclude<
 | 
			
		||||
    GetChronologyItemsQuery["chronologyItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  items: NonNullable<
 | 
			
		||||
    GetChronologyItemsQuery["chronologyItems"]
 | 
			
		||||
  >["data"][number][];
 | 
			
		||||
  langui: AppStaticProps["langui"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ChronologyYearComponent(props: Props): JSX.Element {
 | 
			
		||||
export function ChronologyYearComponent(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import useDarkMode from "hooks/useDarkMode";
 | 
			
		||||
import useStateWithLocalStorage from "hooks/useStateWithLocalStorage";
 | 
			
		||||
import React, { ReactNode, useContext } from "react";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useDarkMode } from "hooks/useDarkMode";
 | 
			
		||||
import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
 | 
			
		||||
import React, { ReactNode, useContext, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface AppLayoutState {
 | 
			
		||||
  subPanelOpen: boolean | undefined;
 | 
			
		||||
@ -14,6 +15,7 @@ interface AppLayoutState {
 | 
			
		||||
  currency: string | undefined;
 | 
			
		||||
  playerName: string | undefined;
 | 
			
		||||
  preferredLanguages: string[] | undefined;
 | 
			
		||||
  menuGestures: boolean;
 | 
			
		||||
  setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
 | 
			
		||||
  setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
 | 
			
		||||
  setMainPanelReduced: React.Dispatch<
 | 
			
		||||
@ -31,6 +33,7 @@ interface AppLayoutState {
 | 
			
		||||
  setPreferredLanguages: React.Dispatch<
 | 
			
		||||
    React.SetStateAction<string[] | undefined>
 | 
			
		||||
  >;
 | 
			
		||||
  setMenuGestures: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* eslint-disable @typescript-eslint/no-empty-function */
 | 
			
		||||
@ -46,6 +49,7 @@ const initialState: AppLayoutState = {
 | 
			
		||||
  currency: "USD",
 | 
			
		||||
  playerName: "",
 | 
			
		||||
  preferredLanguages: [],
 | 
			
		||||
  menuGestures: true,
 | 
			
		||||
  setSubPanelOpen: () => {},
 | 
			
		||||
  setMainPanelReduced: () => {},
 | 
			
		||||
  setMainPanelOpen: () => {},
 | 
			
		||||
@ -57,6 +61,7 @@ const initialState: AppLayoutState = {
 | 
			
		||||
  setCurrency: () => {},
 | 
			
		||||
  setPlayerName: () => {},
 | 
			
		||||
  setPreferredLanguages: () => {},
 | 
			
		||||
  setMenuGestures: () => {},
 | 
			
		||||
};
 | 
			
		||||
/* eslint-enable @typescript-eslint/no-empty-function */
 | 
			
		||||
 | 
			
		||||
@ -72,7 +77,7 @@ interface Props {
 | 
			
		||||
  children: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function AppContextProvider(props: Props): JSX.Element {
 | 
			
		||||
export function AppContextProvider(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage<
 | 
			
		||||
    boolean | undefined
 | 
			
		||||
  >("subPanelOpen", initialState.subPanelOpen);
 | 
			
		||||
@ -115,6 +120,8 @@ export function AppContextProvider(props: Props): JSX.Element {
 | 
			
		||||
    string[] | undefined
 | 
			
		||||
  >("preferredLanguages", initialState.preferredLanguages);
 | 
			
		||||
 | 
			
		||||
  const [menuGestures, setMenuGestures] = useState(false);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AppContext.Provider
 | 
			
		||||
      value={{
 | 
			
		||||
@ -129,6 +136,7 @@ export function AppContextProvider(props: Props): JSX.Element {
 | 
			
		||||
        currency,
 | 
			
		||||
        playerName,
 | 
			
		||||
        preferredLanguages,
 | 
			
		||||
        menuGestures,
 | 
			
		||||
        setSubPanelOpen,
 | 
			
		||||
        setConfigPanelOpen,
 | 
			
		||||
        setMainPanelReduced,
 | 
			
		||||
@ -140,6 +148,7 @@ export function AppContextProvider(props: Props): JSX.Element {
 | 
			
		||||
        setCurrency,
 | 
			
		||||
        setPlayerName,
 | 
			
		||||
        setPreferredLanguages,
 | 
			
		||||
        setMenuGestures,
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {props.children}
 | 
			
		||||
 | 
			
		||||
@ -4,22 +4,18 @@ import {
 | 
			
		||||
  GetWebsiteInterfaceQuery,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
 | 
			
		||||
export interface AppStaticProps {
 | 
			
		||||
  langui: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      GetWebsiteInterfaceQuery["websiteInterfaces"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["data"][number]["attributes"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
export type AppStaticProps = Immutable<{
 | 
			
		||||
  langui: NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      GetWebsiteInterfaceQuery["websiteInterfaces"]
 | 
			
		||||
    >["data"][number]["attributes"]
 | 
			
		||||
  >;
 | 
			
		||||
  currencies: Exclude<
 | 
			
		||||
    GetCurrenciesQuery["currencies"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"];
 | 
			
		||||
  languages: Exclude<GetLanguagesQuery["languages"], null | undefined>["data"];
 | 
			
		||||
}
 | 
			
		||||
  currencies: NonNullable<GetCurrenciesQuery["currencies"]>["data"];
 | 
			
		||||
  languages: NonNullable<GetLanguagesQuery["languages"]>["data"];
 | 
			
		||||
}>;
 | 
			
		||||
 | 
			
		||||
export async function getAppStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
@ -50,9 +46,11 @@ export async function getAppStaticProps(
 | 
			
		||||
    })
 | 
			
		||||
  ).websiteInterfaces?.data[0].attributes;
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    langui: langui ?? ({} as AppStaticProps["langui"]),
 | 
			
		||||
    currencies: currencies?.data ?? ({} as AppStaticProps["currencies"]),
 | 
			
		||||
    languages: languages?.data ?? ({} as AppStaticProps["languages"]),
 | 
			
		||||
  const appStaticProps: AppStaticProps = {
 | 
			
		||||
    langui: langui ?? {},
 | 
			
		||||
    currencies: currencies?.data ?? [],
 | 
			
		||||
    languages: languages?.data ?? [],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return appStaticProps;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/graphql/getPostStaticProps.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/graphql/getPostStaticProps.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import { PostWithTranslations } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "./getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "./sdk";
 | 
			
		||||
 | 
			
		||||
export interface PostStaticProps extends AppStaticProps {
 | 
			
		||||
  post: PostWithTranslations;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPostStaticProps(
 | 
			
		||||
  slug: string
 | 
			
		||||
): (
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
) => Promise<{ notFound: boolean } | { props: PostStaticProps }> {
 | 
			
		||||
  return async (context: GetStaticPropsContext) => {
 | 
			
		||||
    const sdk = getReadySdk();
 | 
			
		||||
    const post = await sdk.getPost({
 | 
			
		||||
      slug: slug,
 | 
			
		||||
      language_code: context.locale ?? "en",
 | 
			
		||||
    });
 | 
			
		||||
    if (post.posts?.data[0].attributes?.translations) {
 | 
			
		||||
      const props: PostStaticProps = {
 | 
			
		||||
        ...(await getAppStaticProps(context)),
 | 
			
		||||
        post: post.posts.data[0].attributes as PostWithTranslations,
 | 
			
		||||
      };
 | 
			
		||||
      return {
 | 
			
		||||
        props: props,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return { notFound: true };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@ -14,7 +14,13 @@ query devGetContents {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        titles {
 | 
			
		||||
 | 
			
		||||
        ranged_contents {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        translations {
 | 
			
		||||
          language {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
@ -22,62 +28,36 @@ query devGetContents {
 | 
			
		||||
          }
 | 
			
		||||
          title
 | 
			
		||||
          description
 | 
			
		||||
        }
 | 
			
		||||
        ranged_contents {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        text_set {
 | 
			
		||||
          language {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
          text_set {
 | 
			
		||||
            source_language {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          source_language {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
            status
 | 
			
		||||
            transcribers {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          status
 | 
			
		||||
          transcribers {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
            translators {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          translators {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
            proofreaders {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            text
 | 
			
		||||
          }
 | 
			
		||||
          proofreaders {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          text
 | 
			
		||||
        }
 | 
			
		||||
        video_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        audio_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        thumbnail {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        next_recommended {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        previous_recommended {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,77 +0,0 @@
 | 
			
		||||
query getContent($slug: String, $language_code: String) {
 | 
			
		||||
  contents(filters: { slug: { eq: $slug } }) {
 | 
			
		||||
    data {
 | 
			
		||||
      attributes {
 | 
			
		||||
        slug
 | 
			
		||||
        titles(filters: { language: { code: { eq: $language_code } } }) {
 | 
			
		||||
          pre_title
 | 
			
		||||
          title
 | 
			
		||||
          subtitle
 | 
			
		||||
          description
 | 
			
		||||
        }
 | 
			
		||||
        categories {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
            attributes {
 | 
			
		||||
              name
 | 
			
		||||
              short
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        type {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
              slug
 | 
			
		||||
              titles(filters: { language: { code: { eq: $language_code } } }) {
 | 
			
		||||
                title
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        ranged_contents {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
            attributes {
 | 
			
		||||
              slug
 | 
			
		||||
              scan_set {
 | 
			
		||||
                id
 | 
			
		||||
              }
 | 
			
		||||
              library_item {
 | 
			
		||||
                data {
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    slug
 | 
			
		||||
                    title
 | 
			
		||||
                    subtitle
 | 
			
		||||
                    thumbnail {
 | 
			
		||||
                      data {
 | 
			
		||||
                        attributes {
 | 
			
		||||
                          ...uploadImage
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        text_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        video_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        audio_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        thumbnail {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
              ...uploadImage
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -4,12 +4,7 @@ query getContentText($slug: String, $language_code: String) {
 | 
			
		||||
      id
 | 
			
		||||
      attributes {
 | 
			
		||||
        slug
 | 
			
		||||
        titles {
 | 
			
		||||
          pre_title
 | 
			
		||||
          title
 | 
			
		||||
          subtitle
 | 
			
		||||
          description
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        categories {
 | 
			
		||||
          data {
 | 
			
		||||
            id
 | 
			
		||||
@ -56,9 +51,7 @@ query getContentText($slug: String, $language_code: String) {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        text_set {
 | 
			
		||||
          status
 | 
			
		||||
          text
 | 
			
		||||
        translations {
 | 
			
		||||
          language {
 | 
			
		||||
            data {
 | 
			
		||||
              attributes {
 | 
			
		||||
@ -66,39 +59,48 @@ query getContentText($slug: String, $language_code: String) {
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          source_language {
 | 
			
		||||
            data {
 | 
			
		||||
              attributes {
 | 
			
		||||
                code
 | 
			
		||||
          pre_title
 | 
			
		||||
          title
 | 
			
		||||
          subtitle
 | 
			
		||||
          description
 | 
			
		||||
          text_set {
 | 
			
		||||
            status
 | 
			
		||||
            text
 | 
			
		||||
            source_language {
 | 
			
		||||
              data {
 | 
			
		||||
                attributes {
 | 
			
		||||
                  code
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          transcribers {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
              attributes {
 | 
			
		||||
                ...recorderChip
 | 
			
		||||
            transcribers {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
                attributes {
 | 
			
		||||
                  ...recorderChip
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          translators {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
              attributes {
 | 
			
		||||
                ...recorderChip
 | 
			
		||||
            translators {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
                attributes {
 | 
			
		||||
                  ...recorderChip
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          proofreaders {
 | 
			
		||||
            data {
 | 
			
		||||
              id
 | 
			
		||||
              attributes {
 | 
			
		||||
                ...recorderChip
 | 
			
		||||
            proofreaders {
 | 
			
		||||
              data {
 | 
			
		||||
                id
 | 
			
		||||
                attributes {
 | 
			
		||||
                  ...recorderChip
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            notes
 | 
			
		||||
          }
 | 
			
		||||
          notes
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        thumbnail {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
@ -106,78 +108,47 @@ query getContentText($slug: String, $language_code: String) {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        previous_recommended {
 | 
			
		||||
        group {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
              slug
 | 
			
		||||
              titles(filters: { language: { code: { eq: $language_code } } }) {
 | 
			
		||||
                pre_title
 | 
			
		||||
                title
 | 
			
		||||
                subtitle
 | 
			
		||||
              }
 | 
			
		||||
              categories {
 | 
			
		||||
                data {
 | 
			
		||||
                  id
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    short
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              type {
 | 
			
		||||
              contents {
 | 
			
		||||
                data {
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    slug
 | 
			
		||||
                    titles(
 | 
			
		||||
                      filters: { language: { code: { eq: $language_code } } }
 | 
			
		||||
                    ) {
 | 
			
		||||
                    translations {
 | 
			
		||||
                      pre_title
 | 
			
		||||
                      title
 | 
			
		||||
                      subtitle
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              thumbnail {
 | 
			
		||||
                data {
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    ...uploadImage
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        next_recommended {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
              slug
 | 
			
		||||
              titles(filters: { language: { code: { eq: $language_code } } }) {
 | 
			
		||||
                pre_title
 | 
			
		||||
                title
 | 
			
		||||
                subtitle
 | 
			
		||||
              }
 | 
			
		||||
              categories {
 | 
			
		||||
                data {
 | 
			
		||||
                  id
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    short
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              type {
 | 
			
		||||
                data {
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    slug
 | 
			
		||||
                    titles(
 | 
			
		||||
                      filters: { language: { code: { eq: $language_code } } }
 | 
			
		||||
                    ) {
 | 
			
		||||
                      title
 | 
			
		||||
                    categories {
 | 
			
		||||
                      data {
 | 
			
		||||
                        id
 | 
			
		||||
                        attributes {
 | 
			
		||||
                          short
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    type {
 | 
			
		||||
                      data {
 | 
			
		||||
                        attributes {
 | 
			
		||||
                          slug
 | 
			
		||||
                          titles(
 | 
			
		||||
                            filters: {
 | 
			
		||||
                              language: { code: { eq: $language_code } }
 | 
			
		||||
                            }
 | 
			
		||||
                          ) {
 | 
			
		||||
                            title
 | 
			
		||||
                          }
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    thumbnail {
 | 
			
		||||
                      data {
 | 
			
		||||
                        attributes {
 | 
			
		||||
                          ...uploadImage
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              thumbnail {
 | 
			
		||||
                data {
 | 
			
		||||
                  attributes {
 | 
			
		||||
                    ...uploadImage
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ query getContents($language_code: String) {
 | 
			
		||||
      id
 | 
			
		||||
      attributes {
 | 
			
		||||
        slug
 | 
			
		||||
        titles(filters: { language: { code: { eq: $language_code } } }) {
 | 
			
		||||
        translations {
 | 
			
		||||
          pre_title
 | 
			
		||||
          title
 | 
			
		||||
          subtitle
 | 
			
		||||
@ -55,14 +55,17 @@ query getContents($language_code: String) {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        text_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        video_set {
 | 
			
		||||
          id
 | 
			
		||||
        }
 | 
			
		||||
        audio_set {
 | 
			
		||||
          id
 | 
			
		||||
        group {
 | 
			
		||||
          data {
 | 
			
		||||
            attributes {
 | 
			
		||||
              combine
 | 
			
		||||
              contents {
 | 
			
		||||
                data {
 | 
			
		||||
                  id
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        thumbnail {
 | 
			
		||||
          data {
 | 
			
		||||
 | 
			
		||||
@ -362,22 +362,18 @@ query getLibraryItem($slug: String, $language_code: String) {
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }
 | 
			
		||||
                    titles(
 | 
			
		||||
                      filters: { language: { code: { eq: $language_code } } }
 | 
			
		||||
                    ) {
 | 
			
		||||
                    translations {
 | 
			
		||||
                      language {
 | 
			
		||||
                        data {
 | 
			
		||||
                          attributes {
 | 
			
		||||
                            code
 | 
			
		||||
                          }
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                      pre_title
 | 
			
		||||
                      title
 | 
			
		||||
                      subtitle
 | 
			
		||||
                    }
 | 
			
		||||
                    text_set {
 | 
			
		||||
                      id
 | 
			
		||||
                    }
 | 
			
		||||
                    video_set {
 | 
			
		||||
                      id
 | 
			
		||||
                    }
 | 
			
		||||
                    audio_set {
 | 
			
		||||
                      id
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,19 @@ query getWebsiteInterface($language_code: String) {
 | 
			
		||||
        response_invalid_code
 | 
			
		||||
        response_invalid_email
 | 
			
		||||
        response_email_success
 | 
			
		||||
        always_show_info
 | 
			
		||||
        item_not_available
 | 
			
		||||
        primary_language
 | 
			
		||||
        secondary_language
 | 
			
		||||
        combine_related_contents
 | 
			
		||||
        previous_content
 | 
			
		||||
        followup_content
 | 
			
		||||
        videos
 | 
			
		||||
        view_on
 | 
			
		||||
        channel
 | 
			
		||||
        subscribers
 | 
			
		||||
        description
 | 
			
		||||
        available_at
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								src/helpers/contents.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/helpers/contents.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
import { ContentWithTranslations, Immutable } from "./types";
 | 
			
		||||
 | 
			
		||||
type Group = Immutable<
 | 
			
		||||
  NonNullable<
 | 
			
		||||
    NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        NonNullable<ContentWithTranslations["group"]>["data"]
 | 
			
		||||
      >["attributes"]
 | 
			
		||||
    >["contents"]
 | 
			
		||||
  >["data"]
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export function getPreviousContent(group: Group, currentSlug: string) {
 | 
			
		||||
  for (let index = 0; index < group.length; index += 1) {
 | 
			
		||||
    const content = group[index];
 | 
			
		||||
    if (content.attributes?.slug === currentSlug && index > 0) {
 | 
			
		||||
      return group[index - 1];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNextContent(group: Group, currentSlug: string) {
 | 
			
		||||
  for (let index = 0; index < group.length; index += 1) {
 | 
			
		||||
    const content = group[index];
 | 
			
		||||
    if (content.attributes?.slug === currentSlug && index < group.length - 1) {
 | 
			
		||||
      return group[index + 1];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
@ -1,20 +1,9 @@
 | 
			
		||||
import {
 | 
			
		||||
  getAssetURL,
 | 
			
		||||
  getImgSizesByQuality,
 | 
			
		||||
  ImageQuality,
 | 
			
		||||
} from "components/Img";
 | 
			
		||||
import {
 | 
			
		||||
  DatePickerFragment,
 | 
			
		||||
  Enum_Componentsetstextset_Status,
 | 
			
		||||
  GetCurrenciesQuery,
 | 
			
		||||
  GetLibraryItemQuery,
 | 
			
		||||
  GetLibraryItemScansQuery,
 | 
			
		||||
  PricePickerFragment,
 | 
			
		||||
  UploadImageFragment,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "./getAppStaticProps";
 | 
			
		||||
import { DatePickerFragment, PricePickerFragment } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "../graphql/getAppStaticProps";
 | 
			
		||||
import { convertPrice } from "./numbers";
 | 
			
		||||
import { Immutable } from "./types";
 | 
			
		||||
 | 
			
		||||
export function prettyDate(datePicker: DatePickerFragment): string {
 | 
			
		||||
export function prettyDate(datePicker: Immutable<DatePickerFragment>): string {
 | 
			
		||||
  let result = "";
 | 
			
		||||
  if (datePicker.year) result += datePicker.year.toString();
 | 
			
		||||
  if (datePicker.month)
 | 
			
		||||
@ -25,7 +14,7 @@ export function prettyDate(datePicker: DatePickerFragment): string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function prettyPrice(
 | 
			
		||||
  pricePicker: PricePickerFragment,
 | 
			
		||||
  pricePicker: Immutable<PricePickerFragment>,
 | 
			
		||||
  currencies: AppStaticProps["currencies"],
 | 
			
		||||
  targetCurrencyCode?: string
 | 
			
		||||
): string {
 | 
			
		||||
@ -45,25 +34,6 @@ export function prettyPrice(
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertPrice(
 | 
			
		||||
  pricePicker: PricePickerFragment,
 | 
			
		||||
  targetCurrency: Exclude<
 | 
			
		||||
    GetCurrenciesQuery["currencies"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number]
 | 
			
		||||
): number {
 | 
			
		||||
  if (
 | 
			
		||||
    pricePicker.amount &&
 | 
			
		||||
    pricePicker.currency?.data?.attributes &&
 | 
			
		||||
    targetCurrency.attributes
 | 
			
		||||
  )
 | 
			
		||||
    return (
 | 
			
		||||
      (pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
 | 
			
		||||
      targetCurrency.attributes.rate_to_usd
 | 
			
		||||
    );
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function prettySlug(slug?: string, parentSlug?: string): string {
 | 
			
		||||
  if (slug) {
 | 
			
		||||
    if (parentSlug && slug.startsWith(parentSlug))
 | 
			
		||||
@ -88,7 +58,7 @@ export function prettyinlineTitle(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function prettyItemType(
 | 
			
		||||
  metadata: any,
 | 
			
		||||
  metadata: Immutable<any>,
 | 
			
		||||
  langui: AppStaticProps["langui"]
 | 
			
		||||
): string | undefined | null {
 | 
			
		||||
  switch (metadata.__typename) {
 | 
			
		||||
@ -110,7 +80,7 @@ export function prettyItemType(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function prettyItemSubType(
 | 
			
		||||
  metadata:
 | 
			
		||||
  metadata: Immutable<
 | 
			
		||||
    | {
 | 
			
		||||
        __typename: "ComponentMetadataAudio";
 | 
			
		||||
        subtype?: {
 | 
			
		||||
@ -187,6 +157,7 @@ export function prettyItemSubType(
 | 
			
		||||
      }
 | 
			
		||||
    | { __typename: "Error" }
 | 
			
		||||
    | null
 | 
			
		||||
  >
 | 
			
		||||
): string {
 | 
			
		||||
  if (metadata) {
 | 
			
		||||
    switch (metadata.__typename) {
 | 
			
		||||
@ -300,87 +271,6 @@ export function capitalizeString(string: string): string {
 | 
			
		||||
  return words.join(" ");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertMmToInch(mm: number | null | undefined): string {
 | 
			
		||||
  return mm ? (mm * 0.03937008).toPrecision(3) : "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OgImage {
 | 
			
		||||
  image: string;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  alt: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getOgImage(
 | 
			
		||||
  quality: ImageQuality,
 | 
			
		||||
  image: UploadImageFragment
 | 
			
		||||
): OgImage {
 | 
			
		||||
  const imgSize = getImgSizesByQuality(
 | 
			
		||||
    image.width ?? 0,
 | 
			
		||||
    image.height ?? 0,
 | 
			
		||||
    quality ? quality : ImageQuality.Small
 | 
			
		||||
  );
 | 
			
		||||
  return {
 | 
			
		||||
    image: getAssetURL(image.url, quality),
 | 
			
		||||
    width: imgSize.width,
 | 
			
		||||
    height: imgSize.height,
 | 
			
		||||
    alt: image.alternativeText || "",
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sortContent(
 | 
			
		||||
  contents:
 | 
			
		||||
    | Exclude<
 | 
			
		||||
        Exclude<
 | 
			
		||||
          GetLibraryItemQuery["libraryItems"],
 | 
			
		||||
          null | undefined
 | 
			
		||||
        >["data"][number]["attributes"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["contents"]
 | 
			
		||||
    | Exclude<
 | 
			
		||||
        Exclude<
 | 
			
		||||
          GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
          null | undefined
 | 
			
		||||
        >["data"][number]["attributes"],
 | 
			
		||||
        null | undefined
 | 
			
		||||
      >["contents"]
 | 
			
		||||
) {
 | 
			
		||||
  contents?.data.sort((a, b) => {
 | 
			
		||||
    if (
 | 
			
		||||
      a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
 | 
			
		||||
      b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
 | 
			
		||||
    ) {
 | 
			
		||||
      return (
 | 
			
		||||
        a.attributes.range[0].starting_page -
 | 
			
		||||
        b.attributes.range[0].starting_page
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getStatusDescription(
 | 
			
		||||
  status: string,
 | 
			
		||||
  langui: AppStaticProps["langui"]
 | 
			
		||||
): string | null | undefined {
 | 
			
		||||
  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 "";
 | 
			
		||||
@ -400,51 +290,3 @@ export function slugify(string: string | undefined): string {
 | 
			
		||||
    .trim()
 | 
			
		||||
    .replace(/ /gu, "-");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function randomInt(min: number, max: number) {
 | 
			
		||||
  return Math.floor(Math.random() * (max - min)) + min;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getLocalesFromLanguages(
 | 
			
		||||
  languages?: Array<{
 | 
			
		||||
    language?: {
 | 
			
		||||
      data?: {
 | 
			
		||||
        attributes?: { code: string } | null;
 | 
			
		||||
      } | null;
 | 
			
		||||
    } | null;
 | 
			
		||||
  } | null> | null
 | 
			
		||||
) {
 | 
			
		||||
  return languages
 | 
			
		||||
    ? languages.map((language) => language?.language?.data?.attributes?.code)
 | 
			
		||||
    : [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getVideoThumbnailURL(uid: string): string {
 | 
			
		||||
  return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getVideoFile(uid: string): string {
 | 
			
		||||
  return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function arrayMove<T>(arr: T[], old_index: number, new_index: number) {
 | 
			
		||||
  arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
 | 
			
		||||
  return arr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getPreferredLanguage(
 | 
			
		||||
  preferredLanguages: (string | undefined)[],
 | 
			
		||||
  availableLanguages: Map<string, number>
 | 
			
		||||
): number | undefined {
 | 
			
		||||
  for (const locale of preferredLanguages) {
 | 
			
		||||
    if (locale && availableLanguages.has(locale)) {
 | 
			
		||||
      return availableLanguages.get(locale);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isInteger(value: string): boolean {
 | 
			
		||||
  // eslint-disable-next-line require-unicode-regexp
 | 
			
		||||
  return /^[+-]?[0-9]+$/.test(value);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								src/helpers/img.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/helpers/img.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
import { UploadImageFragment } from "graphql/generated";
 | 
			
		||||
import { Immutable } from "./types";
 | 
			
		||||
 | 
			
		||||
export enum ImageQuality {
 | 
			
		||||
  Small = "small",
 | 
			
		||||
  Medium = "medium",
 | 
			
		||||
  Large = "large",
 | 
			
		||||
  Og = "og",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OgImage {
 | 
			
		||||
  image: string;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  alt: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAssetFilename(path: string): string {
 | 
			
		||||
  let result = path.split("/");
 | 
			
		||||
  result = result[result.length - 1].split(".");
 | 
			
		||||
  result = result
 | 
			
		||||
    .splice(0, result.length - 1)
 | 
			
		||||
    .join(".")
 | 
			
		||||
    .split("_");
 | 
			
		||||
  return result[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAssetURL(
 | 
			
		||||
  url: string,
 | 
			
		||||
  quality: Immutable<ImageQuality>
 | 
			
		||||
): string {
 | 
			
		||||
  let newUrl = url;
 | 
			
		||||
  newUrl = newUrl.replace(/^\/uploads/u, `/${quality}`);
 | 
			
		||||
  newUrl = newUrl.replace(/.jpg$/u, ".webp");
 | 
			
		||||
  newUrl = newUrl.replace(/.jpeg$/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(
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  maxSize: number
 | 
			
		||||
): { width: number; height: number } {
 | 
			
		||||
  if (width > height) {
 | 
			
		||||
    if (width < maxSize) return { width: width, height: height };
 | 
			
		||||
    return { width: maxSize, height: (height / width) * maxSize };
 | 
			
		||||
  }
 | 
			
		||||
  if (height < maxSize) return { width: width, height: height };
 | 
			
		||||
  return { width: (width / height) * maxSize, height: maxSize };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getImgSizesByQuality(
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  quality: ImageQuality
 | 
			
		||||
): { width: number; height: number } {
 | 
			
		||||
  switch (quality) {
 | 
			
		||||
    case ImageQuality.Og:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 512);
 | 
			
		||||
    case ImageQuality.Small:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 512);
 | 
			
		||||
    case ImageQuality.Medium:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 1024);
 | 
			
		||||
    case ImageQuality.Large:
 | 
			
		||||
      return getImgSizesByMaxSize(width, height, 2048);
 | 
			
		||||
    default:
 | 
			
		||||
      return { width: 0, height: 0 };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getOgImage(
 | 
			
		||||
  quality: Immutable<ImageQuality>,
 | 
			
		||||
  image: Immutable<UploadImageFragment>
 | 
			
		||||
): OgImage {
 | 
			
		||||
  const imgSize = getImgSizesByQuality(
 | 
			
		||||
    image.width ?? 0,
 | 
			
		||||
    image.height ?? 0,
 | 
			
		||||
    quality ? quality : ImageQuality.Small
 | 
			
		||||
  );
 | 
			
		||||
  return {
 | 
			
		||||
    image: getAssetURL(image.url, quality),
 | 
			
		||||
    width: imgSize.width,
 | 
			
		||||
    height: imgSize.height,
 | 
			
		||||
    alt: image.alternativeText || "",
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/helpers/numbers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/helpers/numbers.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
import { GetCurrenciesQuery, PricePickerFragment } from "graphql/generated";
 | 
			
		||||
import { Immutable } from "./types";
 | 
			
		||||
 | 
			
		||||
export function convertPrice(
 | 
			
		||||
  pricePicker: Immutable<PricePickerFragment>,
 | 
			
		||||
  targetCurrency: Immutable<
 | 
			
		||||
    NonNullable<GetCurrenciesQuery["currencies"]>["data"][number]
 | 
			
		||||
  >
 | 
			
		||||
): number {
 | 
			
		||||
  if (
 | 
			
		||||
    pricePicker.amount &&
 | 
			
		||||
    pricePicker.currency?.data?.attributes &&
 | 
			
		||||
    targetCurrency.attributes
 | 
			
		||||
  )
 | 
			
		||||
    return (
 | 
			
		||||
      (pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
 | 
			
		||||
      targetCurrency.attributes.rate_to_usd
 | 
			
		||||
    );
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function convertMmToInch(mm: number | null | undefined): string {
 | 
			
		||||
  return mm ? (mm * 0.03937008).toPrecision(3) : "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function randomInt(min: number, max: number) {
 | 
			
		||||
  return Math.floor(Math.random() * (max - min)) + min;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isInteger(value: string): boolean {
 | 
			
		||||
  // eslint-disable-next-line require-unicode-regexp
 | 
			
		||||
  return /^[+-]?[0-9]+$/.test(value);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/helpers/others.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/helpers/others.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
import {
 | 
			
		||||
  Enum_Componentsetstextset_Status,
 | 
			
		||||
  GetLibraryItemQuery,
 | 
			
		||||
  GetLibraryItemScansQuery,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { AppStaticProps } from "../graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "./types";
 | 
			
		||||
 | 
			
		||||
type SortContentProps =
 | 
			
		||||
  | NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        GetLibraryItemQuery["libraryItems"]
 | 
			
		||||
      >["data"][number]["attributes"]
 | 
			
		||||
    >["contents"]
 | 
			
		||||
  | NonNullable<
 | 
			
		||||
      NonNullable<
 | 
			
		||||
        GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
      >["data"][number]["attributes"]
 | 
			
		||||
    >["contents"];
 | 
			
		||||
 | 
			
		||||
export function sortContent(contents: Immutable<SortContentProps>) {
 | 
			
		||||
  if (contents) {
 | 
			
		||||
    const newContent = { ...contents } as SortContentProps;
 | 
			
		||||
    newContent?.data.sort((a, b) => {
 | 
			
		||||
      if (
 | 
			
		||||
        a.attributes?.range[0]?.__typename === "ComponentRangePageRange" &&
 | 
			
		||||
        b.attributes?.range[0]?.__typename === "ComponentRangePageRange"
 | 
			
		||||
      ) {
 | 
			
		||||
        return (
 | 
			
		||||
          a.attributes.range[0].starting_page -
 | 
			
		||||
          b.attributes.range[0].starting_page
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return 0;
 | 
			
		||||
    });
 | 
			
		||||
    return newContent as Immutable<SortContentProps>;
 | 
			
		||||
  }
 | 
			
		||||
  return contents;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getStatusDescription(
 | 
			
		||||
  status: string,
 | 
			
		||||
  langui: AppStaticProps["langui"]
 | 
			
		||||
): string | null | undefined {
 | 
			
		||||
  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 arrayMove<T>(arr: T[], old_index: number, new_index: number) {
 | 
			
		||||
  arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
 | 
			
		||||
  return arr;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/helpers/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/helpers/types.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
import { GetContentTextQuery, GetPostQuery } from "graphql/generated";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
type Post = NonNullable<
 | 
			
		||||
  NonNullable<GetPostQuery["posts"]>["data"][number]["attributes"]
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export interface PostWithTranslations extends Omit<Post, "translations"> {
 | 
			
		||||
  translations: NonNullable<Post["translations"]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Content = NonNullable<
 | 
			
		||||
  NonNullable<GetContentTextQuery["contents"]>["data"][number]["attributes"]
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export interface ContentWithTranslations extends Omit<Content, "translations"> {
 | 
			
		||||
  translations: NonNullable<Content["translations"]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ImmutableBlackList<T> = JSX.Element | React.ReactNode | Function;
 | 
			
		||||
 | 
			
		||||
export type Immutable<T> = {
 | 
			
		||||
  readonly [K in keyof T]: T[K] extends ImmutableBlackList<T>
 | 
			
		||||
    ? T[K]
 | 
			
		||||
    : Immutable<T[K]>;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										7
									
								
								src/helpers/videos.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/helpers/videos.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
export function getVideoThumbnailURL(uid: string): string {
 | 
			
		||||
  return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.webp`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getVideoFile(uid: string): string {
 | 
			
		||||
  return `${process.env.NEXT_PUBLIC_URL_WATCH}/videos/${uid}.mp4`;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { useEffect } from "react";
 | 
			
		||||
import { usePrefersDarkMode } from "./useMediaQuery";
 | 
			
		||||
import useStateWithLocalStorage from "./useStateWithLocalStorage";
 | 
			
		||||
import { useStateWithLocalStorage } from "./useStateWithLocalStorage";
 | 
			
		||||
 | 
			
		||||
export default function useDarkMode(
 | 
			
		||||
export function useDarkMode(
 | 
			
		||||
  key: string,
 | 
			
		||||
  initialValue: boolean | undefined
 | 
			
		||||
): [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										28
									
								
								src/hooks/useLightBox.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/hooks/useLightBox.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import { LightBox } from "components/LightBox";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
export function useLightBox(): [
 | 
			
		||||
  (images: string[], index?: number) => void,
 | 
			
		||||
  () => JSX.Element
 | 
			
		||||
] {
 | 
			
		||||
  const [lightboxOpen, setLightboxOpen] = useState(false);
 | 
			
		||||
  const [lightboxImages, setLightboxImages] = useState([""]);
 | 
			
		||||
  const [lightboxIndex, setLightboxIndex] = useState(0);
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    (images: string[], index = 0) => {
 | 
			
		||||
      setLightboxOpen(true);
 | 
			
		||||
      setLightboxImages(images);
 | 
			
		||||
      setLightboxIndex(index);
 | 
			
		||||
    },
 | 
			
		||||
    () => (
 | 
			
		||||
      <LightBox
 | 
			
		||||
        state={lightboxOpen}
 | 
			
		||||
        setState={setLightboxOpen}
 | 
			
		||||
        images={lightboxImages}
 | 
			
		||||
        index={lightboxIndex}
 | 
			
		||||
        setIndex={setLightboxIndex}
 | 
			
		||||
      />
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
export default function useMediaQuery(query: string): boolean {
 | 
			
		||||
export function useMediaQuery(query: string): boolean {
 | 
			
		||||
  function getMatches(query: string): boolean {
 | 
			
		||||
    // Prevents SSR issues
 | 
			
		||||
    if (typeof window !== "undefined") {
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,32 @@
 | 
			
		||||
import LanguageSwitcher from "components/Inputs/LanguageSwitcher";
 | 
			
		||||
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { AppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getPreferredLanguage } from "queries/helpers";
 | 
			
		||||
import { useEffect, useMemo, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props<T> {
 | 
			
		||||
  items: T[];
 | 
			
		||||
  items: Immutable<T[]>;
 | 
			
		||||
  languages: AppStaticProps["languages"];
 | 
			
		||||
  languageExtractor: (item: T) => string | undefined;
 | 
			
		||||
  transform?: (item: T) => T;
 | 
			
		||||
  languageExtractor: (item: NonNullable<Immutable<T>>) => string | undefined;
 | 
			
		||||
  transform?: (item: NonNullable<Immutable<T>>) => NonNullable<Immutable<T>>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function useSmartLanguage<T>(
 | 
			
		||||
function getPreferredLanguage(
 | 
			
		||||
  preferredLanguages: (string | undefined)[],
 | 
			
		||||
  availableLanguages: Map<string, number>
 | 
			
		||||
): number | undefined {
 | 
			
		||||
  for (const locale of preferredLanguages) {
 | 
			
		||||
    if (locale && availableLanguages.has(locale)) {
 | 
			
		||||
      return availableLanguages.get(locale);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useSmartLanguage<T>(
 | 
			
		||||
  props: Props<T>
 | 
			
		||||
): [T | undefined, () => JSX.Element] {
 | 
			
		||||
): [Immutable<T | undefined>, () => JSX.Element] {
 | 
			
		||||
  const {
 | 
			
		||||
    items,
 | 
			
		||||
    languageExtractor,
 | 
			
		||||
@ -28,12 +40,15 @@ export default function useSmartLanguage<T>(
 | 
			
		||||
  const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
 | 
			
		||||
    number | undefined
 | 
			
		||||
  >();
 | 
			
		||||
  const [selectedTranslation, setSelectedTranslation] = useState<T>();
 | 
			
		||||
  const [selectedTranslation, setSelectedTranslation] =
 | 
			
		||||
    useState<Immutable<T>>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    items.map((elem, index) => {
 | 
			
		||||
      const result = languageExtractor(elem);
 | 
			
		||||
      if (result !== undefined) availableLocales.set(result, index);
 | 
			
		||||
      if (elem !== null && elem !== undefined) {
 | 
			
		||||
        const result = languageExtractor(elem);
 | 
			
		||||
        if (result !== undefined) availableLocales.set(result, index);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }, [availableLocales, items, languageExtractor]);
 | 
			
		||||
 | 
			
		||||
@ -47,8 +62,9 @@ export default function useSmartLanguage<T>(
 | 
			
		||||
  }, [appLayout.preferredLanguages, availableLocales, router.locale]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedTranslationIndex !== undefined)
 | 
			
		||||
    if (selectedTranslationIndex !== undefined) {
 | 
			
		||||
      setSelectedTranslation(transform(items[selectedTranslationIndex]));
 | 
			
		||||
    }
 | 
			
		||||
  }, [items, selectedTranslationIndex, transform]);
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
export default function useStateWithLocalStorage<T>(
 | 
			
		||||
export function useStateWithLocalStorage<T>(
 | 
			
		||||
  key: string,
 | 
			
		||||
  initialValue: T
 | 
			
		||||
): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>] {
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,16 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel from "components/Panels/ContentPanel";
 | 
			
		||||
import { ContentPanel } from "components/Panels/ContentPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function FourOhFour(props: Props): JSX.Element {
 | 
			
		||||
export default function FourOhFour(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <ContentPanel>
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,16 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel from "components/Panels/ContentPanel";
 | 
			
		||||
import { ContentPanel } from "components/Panels/ContentPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function FiveHundred(props: Props): JSX.Element {
 | 
			
		||||
export default function FiveHundred(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <ContentPanel>
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import Document, {
 | 
			
		||||
  NextScript,
 | 
			
		||||
} from "next/document";
 | 
			
		||||
 | 
			
		||||
class MyDocument extends Document {
 | 
			
		||||
export default 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);
 | 
			
		||||
@ -65,5 +65,3 @@ class MyDocument extends Document {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyDocument;
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AccordsHandbook(props: Props): JSX.Element {
 | 
			
		||||
export default function AccordsHandbook(
 | 
			
		||||
  props: Immutable<PostStaticProps>
 | 
			
		||||
): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <PostPage
 | 
			
		||||
@ -23,21 +23,4 @@ export default function AccordsHandbook(props: Props): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = "accords-handbook";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export const getStaticProps = getPostStaticProps("accords-handbook");
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,18 @@
 | 
			
		||||
import InsetBox from "components/InsetBox";
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { InsetBox } from "components/InsetBox";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
import { randomInt } from "helpers/numbers";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useRouter } from "next/router";
 | 
			
		||||
import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { randomInt } from "queries/helpers";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AboutUs(props: Props): JSX.Element {
 | 
			
		||||
export default function AboutUs(
 | 
			
		||||
  props: Immutable<PostStaticProps>
 | 
			
		||||
): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
 | 
			
		||||
  const router = useRouter();
 | 
			
		||||
@ -181,21 +181,4 @@ export default function AboutUs(props: Props): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = "contact";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export const getStaticProps = getPostStaticProps("contact");
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,14 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function AboutUs(props: Props): JSX.Element {
 | 
			
		||||
export default function AboutUs(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,10 @@
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function SiteInformation(props: Props): JSX.Element {
 | 
			
		||||
export default function Legality(props: PostStaticProps): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <PostPage
 | 
			
		||||
@ -23,21 +20,4 @@ export default function SiteInformation(props: Props): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = "legality";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export const getStaticProps = getPostStaticProps("legality");
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,10 @@
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
}
 | 
			
		||||
export default function SharingPolicy(props: Props): JSX.Element {
 | 
			
		||||
export default function SharingPolicy(props: PostStaticProps): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <PostPage
 | 
			
		||||
@ -22,21 +20,4 @@ export default function SharingPolicy(props: Props): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = "sharing-policy";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export const getStaticProps = getPostStaticProps("sharing-policy");
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ export interface RequestMailProps {
 | 
			
		||||
  formName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function Mail(
 | 
			
		||||
export async function Mail(
 | 
			
		||||
  req: NextApiRequest,
 | 
			
		||||
  res: NextApiResponse<ResponseMailProps>
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
@ -70,7 +70,7 @@ type ResponseMailProps = {
 | 
			
		||||
  revalidated: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default async function Mail(
 | 
			
		||||
export async function Mail(
 | 
			
		||||
  req: NextApiRequest,
 | 
			
		||||
  res: NextApiResponse<ResponseMailProps>
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,14 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function Archives(props: Props): JSX.Element {
 | 
			
		||||
export default function Archives(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,30 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { GetVideoChannelQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { getVideoThumbnailURL } from "helpers/videos";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getVideoThumbnailURL } from "queries/helpers";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  channel: Exclude<
 | 
			
		||||
    GetVideoChannelQuery["videoChannels"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  channel: NonNullable<
 | 
			
		||||
    GetVideoChannelQuery["videoChannels"]
 | 
			
		||||
  >["data"][number]["attributes"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,7 +36,7 @@ export default function Channel(props: Props): JSX.Element {
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
      <ReturnButton
 | 
			
		||||
        href="/archives/videos/"
 | 
			
		||||
        title={"Videos"}
 | 
			
		||||
        title={langui.videos}
 | 
			
		||||
        langui={langui}
 | 
			
		||||
        displayOn={ReturnButtonType.desktop}
 | 
			
		||||
        className="mb-10"
 | 
			
		||||
@ -43,12 +44,12 @@ export default function Channel(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
      <PanelHeader
 | 
			
		||||
        icon="movie"
 | 
			
		||||
        title="Videos"
 | 
			
		||||
        title={langui.videos}
 | 
			
		||||
        description={langui.archives_description}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
        <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
        <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </SubPanel>
 | 
			
		||||
@ -60,11 +61,15 @@ export default function Channel(props: Props): JSX.Element {
 | 
			
		||||
        <h1 className="text-3xl">{channel?.title}</h1>
 | 
			
		||||
        <p>{channel?.subscribers.toLocaleString()} subscribers</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="grid gap-8 items-start mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0">
 | 
			
		||||
      <div
 | 
			
		||||
        className="grid gap-8 items-start mobile:grid-cols-2
 | 
			
		||||
        desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]
 | 
			
		||||
        pb-12 border-b-[3px] border-dotted last-of-type:border-0"
 | 
			
		||||
      >
 | 
			
		||||
        {channel?.videos?.data.map((video) => (
 | 
			
		||||
          <>
 | 
			
		||||
            {video.attributes && (
 | 
			
		||||
              <ThumbnailPreview
 | 
			
		||||
              <PreviewCard
 | 
			
		||||
                key={video.id}
 | 
			
		||||
                href={`/archives/videos/v/${video.attributes.uid}`}
 | 
			
		||||
                title={video.attributes.title}
 | 
			
		||||
 | 
			
		||||
@ -1,24 +1,27 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import PageSelector from "components/Inputs/PageSelector";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { PageSelector } from "components/Inputs/PageSelector";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { GetVideosPreviewQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettyDate } from "helpers/formatters";
 | 
			
		||||
import { getVideoThumbnailURL } from "helpers/videos";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getVideoThumbnailURL, prettyDate } from "queries/helpers";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  videos: Exclude<GetVideosPreviewQuery["videos"], null | undefined>["data"];
 | 
			
		||||
  videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Videos(props: Props): JSX.Element {
 | 
			
		||||
@ -64,7 +67,7 @@ export default function Videos(props: Props): JSX.Element {
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
        <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
        <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </SubPanel>
 | 
			
		||||
@ -79,11 +82,15 @@ export default function Videos(props: Props): JSX.Element {
 | 
			
		||||
        className="mb-12"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] pb-12 border-b-[3px] border-dotted last-of-type:border-0">
 | 
			
		||||
      <div
 | 
			
		||||
        className="grid gap-8 items-start thin:grid-cols-1 mobile:grid-cols-2
 | 
			
		||||
        desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]
 | 
			
		||||
        pb-12 border-b-[3px] border-dotted last-of-type:border-0"
 | 
			
		||||
      >
 | 
			
		||||
        {paginatedVideos[page].map((video) => (
 | 
			
		||||
          <>
 | 
			
		||||
            {video.attributes && (
 | 
			
		||||
              <ThumbnailPreview
 | 
			
		||||
              <PreviewCard
 | 
			
		||||
                key={video.id}
 | 
			
		||||
                href={`/archives/videos/v/${video.attributes.uid}`}
 | 
			
		||||
                title={video.attributes.title}
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,33 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import HorizontalLine from "components/HorizontalLine";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import InsetBox from "components/InsetBox";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { InsetBox } from "components/InsetBox";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { GetVideoQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
 | 
			
		||||
import { getVideoFile } from "helpers/videos";
 | 
			
		||||
import { useMediaMobile } from "hooks/useMediaQuery";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { getVideoFile, prettyDate, prettyShortenNumber } from "queries/helpers";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  video: Exclude<
 | 
			
		||||
    Exclude<
 | 
			
		||||
      GetVideoQuery["videos"],
 | 
			
		||||
      null | undefined
 | 
			
		||||
    >["data"][number]["attributes"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  video: NonNullable<
 | 
			
		||||
    NonNullable<GetVideoQuery["videos"]>["data"][number]["attributes"]
 | 
			
		||||
  >;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -40,7 +39,7 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
      <ReturnButton
 | 
			
		||||
        href="/archives/videos/"
 | 
			
		||||
        title={"Videos"}
 | 
			
		||||
        title={langui.videos}
 | 
			
		||||
        langui={langui}
 | 
			
		||||
        displayOn={ReturnButtonType.desktop}
 | 
			
		||||
        className="mb-10"
 | 
			
		||||
@ -56,14 +55,14 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <NavOption
 | 
			
		||||
        title={"Channel"}
 | 
			
		||||
        title={langui.channel}
 | 
			
		||||
        url="#channel"
 | 
			
		||||
        border
 | 
			
		||||
        onClick={() => appLayout.setSubPanelOpen(false)}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <NavOption
 | 
			
		||||
        title={"Description"}
 | 
			
		||||
        title={langui.description}
 | 
			
		||||
        url="#description"
 | 
			
		||||
        border
 | 
			
		||||
        onClick={() => appLayout.setSubPanelOpen(false)}
 | 
			
		||||
@ -98,7 +97,8 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
              className="w-full aspect-video"
 | 
			
		||||
              title="YouTube video player"
 | 
			
		||||
              frameBorder="0"
 | 
			
		||||
              allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
 | 
			
		||||
              allow="accelerometer; autoplay; clipboard-write;
 | 
			
		||||
              encrypted-media; gyroscope; picture-in-picture"
 | 
			
		||||
              allowFullScreen
 | 
			
		||||
            ></iframe>
 | 
			
		||||
          )}
 | 
			
		||||
@ -135,7 +135,7 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noreferrer"
 | 
			
		||||
              >
 | 
			
		||||
                <Button className="!py-0 !px-3">{`View on ${video.source}`}</Button>
 | 
			
		||||
                <Button className="!py-0 !px-3">{`${langui.view_on} ${video.source}`}</Button>
 | 
			
		||||
              </a>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@ -144,7 +144,7 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
        {video.channel?.data?.attributes && (
 | 
			
		||||
          <InsetBox id="channel" className="grid place-items-center">
 | 
			
		||||
            <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-4 text-center">
 | 
			
		||||
              <h2 className="text-2xl">{"Channel"}</h2>
 | 
			
		||||
              <h2 className="text-2xl">{langui.channel}</h2>
 | 
			
		||||
              <div>
 | 
			
		||||
                <Button
 | 
			
		||||
                  href={`/archives/videos/c/${video.channel.data.attributes.uid}`}
 | 
			
		||||
@ -153,8 +153,7 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <p>
 | 
			
		||||
                  {video.channel.data.attributes.subscribers.toLocaleString()}{" "}
 | 
			
		||||
                  subscribers
 | 
			
		||||
                  {`${video.channel.data.attributes.subscribers.toLocaleString()} ${langui.subscribers?.toLowerCase()}`}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -163,7 +162,7 @@ export default function Video(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
        <InsetBox id="description" className="grid place-items-center">
 | 
			
		||||
          <div className="w-[clamp(0px,100%,42rem)] grid place-items-center gap-8">
 | 
			
		||||
            <h2 className="text-2xl">{"Description"}</h2>
 | 
			
		||||
            <h2 className="text-2xl">{langui.description}</h2>
 | 
			
		||||
            <p className="whitespace-pre-line">{video.description}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </InsetBox>
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function Chronicles(props: Props): JSX.Element {
 | 
			
		||||
export default function Chronicles(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
 | 
			
		||||
@ -1,56 +1,60 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import HorizontalLine from "components/HorizontalLine";
 | 
			
		||||
import Markdawn from "components/Markdown/Markdawn";
 | 
			
		||||
import TOC from "components/Markdown/TOC";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { HorizontalLine } from "components/HorizontalLine";
 | 
			
		||||
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 PreviewLine from "components/PreviewLine";
 | 
			
		||||
import RecorderChip from "components/RecorderChip";
 | 
			
		||||
import ThumbnailHeader from "components/ThumbnailHeader";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { GetContentTextQuery } from "graphql/generated";
 | 
			
		||||
import { ContentPanel } from "components/Panels/ContentPanel";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewLine } from "components/PreviewLine";
 | 
			
		||||
import { RecorderChip } from "components/RecorderChip";
 | 
			
		||||
import { ThumbnailHeader } from "components/ThumbnailHeader";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { getNextContent, getPreviousContent } from "helpers/contents";
 | 
			
		||||
import {
 | 
			
		||||
  prettyinlineTitle,
 | 
			
		||||
  prettyLanguage,
 | 
			
		||||
  prettySlug,
 | 
			
		||||
} from "helpers/formatters";
 | 
			
		||||
import { getStatusDescription } from "helpers/others";
 | 
			
		||||
import { ContentWithTranslations, Immutable } from "helpers/types";
 | 
			
		||||
import { useMediaMobile } from "hooks/useMediaQuery";
 | 
			
		||||
import useSmartLanguage from "hooks/useSmartLanguage";
 | 
			
		||||
import { useSmartLanguage } from "hooks/useSmartLanguage";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import {
 | 
			
		||||
  getStatusDescription,
 | 
			
		||||
  prettyinlineTitle,
 | 
			
		||||
  prettyLanguage,
 | 
			
		||||
  prettySlug,
 | 
			
		||||
} from "queries/helpers";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  content: Exclude<
 | 
			
		||||
    GetContentTextQuery["contents"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number]["attributes"];
 | 
			
		||||
  contentId: Exclude<
 | 
			
		||||
    GetContentTextQuery["contents"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number]["id"];
 | 
			
		||||
  content: ContentWithTranslations;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Content(props: Props): JSX.Element {
 | 
			
		||||
export default function Content(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui, content, languages } = props;
 | 
			
		||||
  const isMobile = useMediaMobile();
 | 
			
		||||
 | 
			
		||||
  const [selectedTextSet, LanguageSwitcher] = useSmartLanguage({
 | 
			
		||||
    items: content?.text_set,
 | 
			
		||||
  const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({
 | 
			
		||||
    items: content.translations,
 | 
			
		||||
    languages: languages,
 | 
			
		||||
    languageExtractor: (item) => item?.language?.data?.attributes?.code,
 | 
			
		||||
    languageExtractor: (item) => item.language?.data?.attributes?.code,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const selectedTitle = content?.titles?.[0];
 | 
			
		||||
  const previousContent = content.group?.data?.attributes?.contents
 | 
			
		||||
    ? getPreviousContent(
 | 
			
		||||
        content.group.data.attributes.contents.data,
 | 
			
		||||
        content.slug
 | 
			
		||||
      )
 | 
			
		||||
    : undefined;
 | 
			
		||||
 | 
			
		||||
  const nextContent = content.group?.data?.attributes?.contents
 | 
			
		||||
    ? getNextContent(content.group.data.attributes.contents.data, content.slug)
 | 
			
		||||
    : undefined;
 | 
			
		||||
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
@ -62,124 +66,133 @@ export default function Content(props: Props): JSX.Element {
 | 
			
		||||
        horizontalLine
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {selectedTextSet?.source_language?.data?.attributes && (
 | 
			
		||||
      {selectedTranslation?.text_set && (
 | 
			
		||||
        <div className="grid gap-5">
 | 
			
		||||
          <h2 className="text-xl">
 | 
			
		||||
            {selectedTextSet.source_language.data.attributes.code ===
 | 
			
		||||
            selectedTextSet.language?.data?.attributes?.code
 | 
			
		||||
            {selectedTranslation.text_set.source_language?.data?.attributes
 | 
			
		||||
              ?.code === selectedTranslation.language?.data?.attributes?.code
 | 
			
		||||
              ? langui.transcript_notice
 | 
			
		||||
              : langui.translation_notice}
 | 
			
		||||
          </h2>
 | 
			
		||||
 | 
			
		||||
          {selectedTextSet.source_language.data.attributes.code !==
 | 
			
		||||
            selectedTextSet.language?.data?.attributes?.code && (
 | 
			
		||||
            <div className="grid place-items-center gap-2">
 | 
			
		||||
              <p className="font-headers">{langui.source_language}:</p>
 | 
			
		||||
              <Chip>
 | 
			
		||||
                {prettyLanguage(
 | 
			
		||||
                  selectedTextSet.source_language.data.attributes.code,
 | 
			
		||||
                  languages
 | 
			
		||||
                )}
 | 
			
		||||
              </Chip>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          {selectedTranslation.text_set.source_language?.data?.attributes
 | 
			
		||||
            ?.code &&
 | 
			
		||||
            selectedTranslation.text_set.source_language.data.attributes
 | 
			
		||||
              .code !==
 | 
			
		||||
              selectedTranslation.language?.data?.attributes?.code && (
 | 
			
		||||
              <div className="grid place-items-center gap-2">
 | 
			
		||||
                <p className="font-headers">{langui.source_language}:</p>
 | 
			
		||||
                <Chip>
 | 
			
		||||
                  {prettyLanguage(
 | 
			
		||||
                    selectedTranslation.text_set.source_language.data.attributes
 | 
			
		||||
                      .code,
 | 
			
		||||
                    languages
 | 
			
		||||
                  )}
 | 
			
		||||
                </Chip>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
          <div className="grid grid-flow-col place-items-center place-content-center gap-2">
 | 
			
		||||
            <p className="font-headers">{langui.status}:</p>
 | 
			
		||||
 | 
			
		||||
            <ToolTip
 | 
			
		||||
              content={getStatusDescription(selectedTextSet.status, langui)}
 | 
			
		||||
              content={getStatusDescription(
 | 
			
		||||
                selectedTranslation.text_set.status,
 | 
			
		||||
                langui
 | 
			
		||||
              )}
 | 
			
		||||
              maxWidth={"20rem"}
 | 
			
		||||
            >
 | 
			
		||||
              <Chip>{selectedTextSet.status}</Chip>
 | 
			
		||||
              <Chip>{selectedTranslation.text_set.status}</Chip>
 | 
			
		||||
            </ToolTip>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {selectedTextSet.transcribers &&
 | 
			
		||||
            selectedTextSet.transcribers.data.length > 0 && (
 | 
			
		||||
          {selectedTranslation.text_set.transcribers &&
 | 
			
		||||
            selectedTranslation.text_set.transcribers.data.length > 0 && (
 | 
			
		||||
              <div>
 | 
			
		||||
                <p className="font-headers">{langui.transcribers}:</p>
 | 
			
		||||
                <div className="grid place-items-center place-content-center gap-2">
 | 
			
		||||
                  {selectedTextSet.transcribers.data.map((recorder) => (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {recorder.attributes && (
 | 
			
		||||
                        <RecorderChip
 | 
			
		||||
                          key={recorder.id}
 | 
			
		||||
                          langui={langui}
 | 
			
		||||
                          recorder={recorder.attributes}
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                    </>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  {selectedTranslation.text_set.transcribers.data.map(
 | 
			
		||||
                    (recorder) => (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {recorder.attributes && (
 | 
			
		||||
                          <RecorderChip
 | 
			
		||||
                            key={recorder.id}
 | 
			
		||||
                            langui={langui}
 | 
			
		||||
                            recorder={recorder.attributes}
 | 
			
		||||
                          />
 | 
			
		||||
                        )}
 | 
			
		||||
                      </>
 | 
			
		||||
                    )
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
          {selectedTextSet.translators &&
 | 
			
		||||
            selectedTextSet.translators.data.length > 0 && (
 | 
			
		||||
          {selectedTranslation.text_set.translators &&
 | 
			
		||||
            selectedTranslation.text_set.translators.data.length > 0 && (
 | 
			
		||||
              <div>
 | 
			
		||||
                <p className="font-headers">{langui.translators}:</p>
 | 
			
		||||
                <div className="grid place-items-center place-content-center gap-2">
 | 
			
		||||
                  {selectedTextSet.translators.data.map((recorder) => (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {recorder.attributes && (
 | 
			
		||||
                        <RecorderChip
 | 
			
		||||
                          key={recorder.id}
 | 
			
		||||
                          langui={langui}
 | 
			
		||||
                          recorder={recorder.attributes}
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                    </>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  {selectedTranslation.text_set.translators.data.map(
 | 
			
		||||
                    (recorder) => (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {recorder.attributes && (
 | 
			
		||||
                          <RecorderChip
 | 
			
		||||
                            key={recorder.id}
 | 
			
		||||
                            langui={langui}
 | 
			
		||||
                            recorder={recorder.attributes}
 | 
			
		||||
                          />
 | 
			
		||||
                        )}
 | 
			
		||||
                      </>
 | 
			
		||||
                    )
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
          {selectedTextSet.proofreaders &&
 | 
			
		||||
            selectedTextSet.proofreaders.data.length > 0 && (
 | 
			
		||||
          {selectedTranslation.text_set.proofreaders &&
 | 
			
		||||
            selectedTranslation.text_set.proofreaders.data.length > 0 && (
 | 
			
		||||
              <div>
 | 
			
		||||
                <p className="font-headers">{langui.proofreaders}:</p>
 | 
			
		||||
                <div className="grid place-items-center place-content-center gap-2">
 | 
			
		||||
                  {selectedTextSet.proofreaders.data.map((recorder) => (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {recorder.attributes && (
 | 
			
		||||
                        <RecorderChip
 | 
			
		||||
                          key={recorder.id}
 | 
			
		||||
                          langui={langui}
 | 
			
		||||
                          recorder={recorder.attributes}
 | 
			
		||||
                        />
 | 
			
		||||
                      )}
 | 
			
		||||
                    </>
 | 
			
		||||
                  ))}
 | 
			
		||||
                  {selectedTranslation.text_set.proofreaders.data.map(
 | 
			
		||||
                    (recorder) => (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {recorder.attributes && (
 | 
			
		||||
                          <RecorderChip
 | 
			
		||||
                            key={recorder.id}
 | 
			
		||||
                            langui={langui}
 | 
			
		||||
                            recorder={recorder.attributes}
 | 
			
		||||
                          />
 | 
			
		||||
                        )}
 | 
			
		||||
                      </>
 | 
			
		||||
                    )
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
 | 
			
		||||
          {selectedTextSet.notes && (
 | 
			
		||||
          {selectedTranslation.text_set.notes && (
 | 
			
		||||
            <div>
 | 
			
		||||
              <p className="font-headers">{"Notes"}:</p>
 | 
			
		||||
              <div className="grid place-items-center place-content-center gap-2">
 | 
			
		||||
                <Markdawn text={selectedTextSet.notes} />
 | 
			
		||||
                <Markdawn text={selectedTranslation.text_set.notes} />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {selectedTextSet && content?.text_set && selectedTextSet.text && (
 | 
			
		||||
      {selectedTranslation?.text_set?.text && (
 | 
			
		||||
        <>
 | 
			
		||||
          <HorizontalLine />
 | 
			
		||||
          <TOC
 | 
			
		||||
            text={selectedTextSet.text}
 | 
			
		||||
            title={
 | 
			
		||||
              content.titles && content.titles.length > 0 && selectedTitle
 | 
			
		||||
                ? prettyinlineTitle(
 | 
			
		||||
                    selectedTitle.pre_title,
 | 
			
		||||
                    selectedTitle.title,
 | 
			
		||||
                    selectedTitle.subtitle
 | 
			
		||||
                  )
 | 
			
		||||
                : prettySlug(content.slug)
 | 
			
		||||
            }
 | 
			
		||||
            text={selectedTranslation.text_set.text}
 | 
			
		||||
            title={prettyinlineTitle(
 | 
			
		||||
              selectedTranslation.pre_title,
 | 
			
		||||
              selectedTranslation.title,
 | 
			
		||||
              selectedTranslation.subtitle
 | 
			
		||||
            )}
 | 
			
		||||
          />
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
@ -188,142 +201,119 @@ export default function Content(props: Props): JSX.Element {
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <ContentPanel>
 | 
			
		||||
      <ReturnButton
 | 
			
		||||
        href={`/contents/${content?.slug}`}
 | 
			
		||||
        href={`/contents/${content.slug}`}
 | 
			
		||||
        title={langui.content}
 | 
			
		||||
        langui={langui}
 | 
			
		||||
        displayOn={ReturnButtonType.mobile}
 | 
			
		||||
        className="mb-10"
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {content && (
 | 
			
		||||
        <div className="grid place-items-center">
 | 
			
		||||
          <ThumbnailHeader
 | 
			
		||||
            thumbnail={content.thumbnail?.data?.attributes}
 | 
			
		||||
            pre_title={
 | 
			
		||||
              selectedTitle?.pre_title ?? content.titles?.[0]?.pre_title
 | 
			
		||||
            }
 | 
			
		||||
            title={selectedTitle?.title ?? content.titles?.[0]?.title}
 | 
			
		||||
            subtitle={selectedTitle?.subtitle ?? content.titles?.[0]?.subtitle}
 | 
			
		||||
            description={
 | 
			
		||||
              selectedTitle?.description ?? content.titles?.[0]?.description
 | 
			
		||||
            }
 | 
			
		||||
            type={content.type}
 | 
			
		||||
            categories={content.categories}
 | 
			
		||||
            langui={langui}
 | 
			
		||||
            languageSwitcher={<LanguageSwitcher />}
 | 
			
		||||
          />
 | 
			
		||||
      <div className="grid place-items-center">
 | 
			
		||||
        <ThumbnailHeader
 | 
			
		||||
          thumbnail={content.thumbnail?.data?.attributes}
 | 
			
		||||
          pre_title={selectedTranslation?.pre_title}
 | 
			
		||||
          title={selectedTranslation?.title}
 | 
			
		||||
          subtitle={selectedTranslation?.subtitle}
 | 
			
		||||
          description={selectedTranslation?.description}
 | 
			
		||||
          type={content.type}
 | 
			
		||||
          categories={content.categories}
 | 
			
		||||
          langui={langui}
 | 
			
		||||
          languageSwitcher={<LanguageSwitcher />}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
          {content.previous_recommended?.data?.attributes && (
 | 
			
		||||
            <div className="mt-12 mb-8 w-full">
 | 
			
		||||
              <h2 className="text-center text-2xl mb-4">Previous content</h2>
 | 
			
		||||
              <PreviewLine
 | 
			
		||||
                href={`/contents/${content.previous_recommended.data.attributes.slug}`}
 | 
			
		||||
                pre_title={
 | 
			
		||||
                  content.previous_recommended.data.attributes.titles?.[0]
 | 
			
		||||
                    ?.pre_title
 | 
			
		||||
                }
 | 
			
		||||
                title={
 | 
			
		||||
                  content.previous_recommended.data.attributes.titles?.[0]
 | 
			
		||||
                    ?.title ??
 | 
			
		||||
                  prettySlug(content.previous_recommended.data.attributes.slug)
 | 
			
		||||
                }
 | 
			
		||||
                subtitle={
 | 
			
		||||
                  content.previous_recommended.data.attributes.titles?.[0]
 | 
			
		||||
                    ?.subtitle
 | 
			
		||||
                }
 | 
			
		||||
                thumbnail={
 | 
			
		||||
                  content.previous_recommended.data.attributes.thumbnail?.data
 | 
			
		||||
                    ?.attributes
 | 
			
		||||
                }
 | 
			
		||||
                thumbnailAspectRatio="3/2"
 | 
			
		||||
                topChips={
 | 
			
		||||
                  isMobile
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : content.previous_recommended.data.attributes.type?.data
 | 
			
		||||
                        ?.attributes
 | 
			
		||||
                    ? [
 | 
			
		||||
                        content.previous_recommended.data.attributes.type.data
 | 
			
		||||
                          .attributes.titles?.[0]
 | 
			
		||||
                          ? content.previous_recommended.data.attributes.type
 | 
			
		||||
                              .data.attributes.titles[0]?.title
 | 
			
		||||
                          : prettySlug(
 | 
			
		||||
                              content.previous_recommended.data.attributes.type
 | 
			
		||||
                                .data.attributes.slug
 | 
			
		||||
                            ),
 | 
			
		||||
                      ]
 | 
			
		||||
                    : undefined
 | 
			
		||||
                }
 | 
			
		||||
                bottomChips={
 | 
			
		||||
                  isMobile
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : content.previous_recommended.data.attributes.categories?.data.map(
 | 
			
		||||
                        (category) => category.attributes?.short ?? ""
 | 
			
		||||
                      )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
        {previousContent?.attributes && (
 | 
			
		||||
          <div className="mt-12 mb-8 w-full">
 | 
			
		||||
            <h2 className="text-center text-2xl mb-4">
 | 
			
		||||
              {langui.previous_content}
 | 
			
		||||
            </h2>
 | 
			
		||||
            <PreviewLine
 | 
			
		||||
              href={`/contents/${previousContent.attributes.slug}`}
 | 
			
		||||
              pre_title={
 | 
			
		||||
                previousContent.attributes.translations?.[0]?.pre_title
 | 
			
		||||
              }
 | 
			
		||||
              title={
 | 
			
		||||
                previousContent.attributes.translations?.[0]?.title ??
 | 
			
		||||
                prettySlug(previousContent.attributes.slug)
 | 
			
		||||
              }
 | 
			
		||||
              subtitle={previousContent.attributes.translations?.[0]?.subtitle}
 | 
			
		||||
              thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
 | 
			
		||||
              thumbnailAspectRatio="3/2"
 | 
			
		||||
              topChips={
 | 
			
		||||
                isMobile
 | 
			
		||||
                  ? undefined
 | 
			
		||||
                  : previousContent.attributes.type?.data?.attributes
 | 
			
		||||
                  ? [
 | 
			
		||||
                      previousContent.attributes.type.data.attributes
 | 
			
		||||
                        .titles?.[0]
 | 
			
		||||
                        ? previousContent.attributes.type.data.attributes
 | 
			
		||||
                            .titles[0]?.title
 | 
			
		||||
                        : prettySlug(
 | 
			
		||||
                            previousContent.attributes.type.data.attributes.slug
 | 
			
		||||
                          ),
 | 
			
		||||
                    ]
 | 
			
		||||
                  : undefined
 | 
			
		||||
              }
 | 
			
		||||
              bottomChips={
 | 
			
		||||
                isMobile
 | 
			
		||||
                  ? undefined
 | 
			
		||||
                  : previousContent.attributes.categories?.data.map(
 | 
			
		||||
                      (category) => category.attributes?.short ?? ""
 | 
			
		||||
                    )
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
          <HorizontalLine />
 | 
			
		||||
        <HorizontalLine />
 | 
			
		||||
 | 
			
		||||
          <Markdawn text={selectedTextSet?.text ?? ""} />
 | 
			
		||||
        <Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
 | 
			
		||||
 | 
			
		||||
          {content.next_recommended?.data?.attributes && (
 | 
			
		||||
            <>
 | 
			
		||||
              <HorizontalLine />
 | 
			
		||||
              <h2 className="text-center text-2xl mb-4">Follow-up content</h2>
 | 
			
		||||
              <PreviewLine
 | 
			
		||||
                href={`/contents/${content.next_recommended.data.attributes.slug}`}
 | 
			
		||||
                pre_title={
 | 
			
		||||
                  content.next_recommended.data.attributes.titles?.[0]
 | 
			
		||||
                    ?.pre_title
 | 
			
		||||
                }
 | 
			
		||||
                title={
 | 
			
		||||
                  content.next_recommended.data.attributes.titles?.[0]?.title ??
 | 
			
		||||
                  prettySlug(content.next_recommended.data.attributes.slug)
 | 
			
		||||
                }
 | 
			
		||||
                subtitle={
 | 
			
		||||
                  content.next_recommended.data.attributes.titles?.[0]?.subtitle
 | 
			
		||||
                }
 | 
			
		||||
                thumbnail={
 | 
			
		||||
                  content.next_recommended.data.attributes.thumbnail?.data
 | 
			
		||||
                    ?.attributes
 | 
			
		||||
                }
 | 
			
		||||
                thumbnailAspectRatio="3/2"
 | 
			
		||||
                topChips={
 | 
			
		||||
                  isMobile
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : content.next_recommended.data.attributes.type?.data
 | 
			
		||||
                        ?.attributes
 | 
			
		||||
                    ? [
 | 
			
		||||
                        content.next_recommended.data.attributes.type.data
 | 
			
		||||
                          .attributes.titles?.[0]
 | 
			
		||||
                          ? content.next_recommended.data.attributes.type.data
 | 
			
		||||
                              .attributes.titles[0]?.title
 | 
			
		||||
                          : prettySlug(
 | 
			
		||||
                              content.next_recommended.data.attributes.type.data
 | 
			
		||||
                                .attributes.slug
 | 
			
		||||
                            ),
 | 
			
		||||
                      ]
 | 
			
		||||
                    : undefined
 | 
			
		||||
                }
 | 
			
		||||
                bottomChips={
 | 
			
		||||
                  isMobile
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : content.next_recommended.data.attributes.categories?.data.map(
 | 
			
		||||
                        (category) => category.attributes?.short ?? ""
 | 
			
		||||
                      )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
        {nextContent?.attributes && (
 | 
			
		||||
          <>
 | 
			
		||||
            <HorizontalLine />
 | 
			
		||||
            <h2 className="text-center text-2xl mb-4">
 | 
			
		||||
              {langui.followup_content}
 | 
			
		||||
            </h2>
 | 
			
		||||
            <PreviewLine
 | 
			
		||||
              href={`/contents/${nextContent.attributes.slug}`}
 | 
			
		||||
              pre_title={nextContent.attributes.translations?.[0]?.pre_title}
 | 
			
		||||
              title={
 | 
			
		||||
                nextContent.attributes.translations?.[0]?.title ??
 | 
			
		||||
                prettySlug(nextContent.attributes.slug)
 | 
			
		||||
              }
 | 
			
		||||
              subtitle={nextContent.attributes.translations?.[0]?.subtitle}
 | 
			
		||||
              thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
 | 
			
		||||
              thumbnailAspectRatio="3/2"
 | 
			
		||||
              topChips={
 | 
			
		||||
                isMobile
 | 
			
		||||
                  ? undefined
 | 
			
		||||
                  : nextContent.attributes.type?.data?.attributes
 | 
			
		||||
                  ? [
 | 
			
		||||
                      nextContent.attributes.type.data.attributes.titles?.[0]
 | 
			
		||||
                        ? nextContent.attributes.type.data.attributes.titles[0]
 | 
			
		||||
                            ?.title
 | 
			
		||||
                        : prettySlug(
 | 
			
		||||
                            nextContent.attributes.type.data.attributes.slug
 | 
			
		||||
                          ),
 | 
			
		||||
                    ]
 | 
			
		||||
                  : undefined
 | 
			
		||||
              }
 | 
			
		||||
              bottomChips={
 | 
			
		||||
                isMobile
 | 
			
		||||
                  ? undefined
 | 
			
		||||
                  : nextContent.attributes.categories?.data.map(
 | 
			
		||||
                      (category) => category.attributes?.short ?? ""
 | 
			
		||||
                    )
 | 
			
		||||
              }
 | 
			
		||||
            />
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </ContentPanel>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  let description = "";
 | 
			
		||||
  if (content?.type?.data) {
 | 
			
		||||
  if (content.type?.data) {
 | 
			
		||||
    description += `${langui.type}: `;
 | 
			
		||||
 | 
			
		||||
    description +=
 | 
			
		||||
@ -332,7 +322,7 @@ export default function Content(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
    description += "\n";
 | 
			
		||||
  }
 | 
			
		||||
  if (content?.categories?.data && content.categories.data.length > 0) {
 | 
			
		||||
  if (content.categories?.data && content.categories.data.length > 0) {
 | 
			
		||||
    description += `${langui.categories}: `;
 | 
			
		||||
    description += content.categories.data
 | 
			
		||||
      .map((category) => category.attributes?.short)
 | 
			
		||||
@ -343,15 +333,15 @@ export default function Content(props: Props): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <AppLayout
 | 
			
		||||
      navTitle={
 | 
			
		||||
        content?.titles && content.titles.length > 0 && content.titles[0]
 | 
			
		||||
        selectedTranslation
 | 
			
		||||
          ? prettyinlineTitle(
 | 
			
		||||
              content.titles[0].pre_title,
 | 
			
		||||
              content.titles[0].title,
 | 
			
		||||
              content.titles[0].subtitle
 | 
			
		||||
              selectedTranslation.pre_title,
 | 
			
		||||
              selectedTranslation.title,
 | 
			
		||||
              selectedTranslation.subtitle
 | 
			
		||||
            )
 | 
			
		||||
          : prettySlug(content?.slug)
 | 
			
		||||
          : prettySlug(content.slug)
 | 
			
		||||
      }
 | 
			
		||||
      thumbnail={content?.thumbnail?.data?.attributes ?? undefined}
 | 
			
		||||
      thumbnail={content.thumbnail?.data?.attributes ?? undefined}
 | 
			
		||||
      contentPanel={contentPanel}
 | 
			
		||||
      subPanel={subPanel}
 | 
			
		||||
      description={description}
 | 
			
		||||
@ -370,12 +360,12 @@ export async function getStaticProps(
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  if (!content.contents || content.contents.data.length === 0)
 | 
			
		||||
  if (!content.contents || !content.contents.data[0].attributes?.translations) {
 | 
			
		||||
    return { notFound: true };
 | 
			
		||||
  }
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    content: content.contents.data[0].attributes,
 | 
			
		||||
    contentId: content.contents.data[0].id,
 | 
			
		||||
    content: content.contents.data[0].attributes as ContentWithTranslations,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
 | 
			
		||||
@ -1,39 +1,51 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Select from "components/Inputs/Select";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Select } from "components/Inputs/Select";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { GetContentsQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettyinlineTitle, prettySlug } from "queries/helpers";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  contents: Exclude<GetContentsQuery["contents"], null | undefined>["data"];
 | 
			
		||||
  contents: NonNullable<GetContentsQuery["contents"]>["data"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GroupContentItems = Map<string, Props["contents"]>;
 | 
			
		||||
type GroupContentItems = Map<string, Immutable<Props["contents"]>>;
 | 
			
		||||
 | 
			
		||||
export default function Contents(props: Props): JSX.Element {
 | 
			
		||||
export default function Contents(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui, contents } = props;
 | 
			
		||||
 | 
			
		||||
  const [groupingMethod, setGroupingMethod] = useState<number>(-1);
 | 
			
		||||
  const [keepInfoVisible, setKeepInfoVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [combineRelatedContent, setCombineRelatedContent] = useState(true);
 | 
			
		||||
 | 
			
		||||
  const [filteredItems, setFilteredItems] = useState(
 | 
			
		||||
    filterContents(combineRelatedContent, contents)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [groups, setGroups] = useState<GroupContentItems>(
 | 
			
		||||
    getGroups(langui, groupingMethod, contents)
 | 
			
		||||
    getGroups(langui, groupingMethod, filteredItems)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setGroups(getGroups(langui, groupingMethod, contents));
 | 
			
		||||
  }, [langui, groupingMethod, contents]);
 | 
			
		||||
    setFilteredItems(filterContents(combineRelatedContent, contents));
 | 
			
		||||
  }, [combineRelatedContent, contents]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setGroups(getGroups(langui, groupingMethod, filteredItems));
 | 
			
		||||
  }, [langui, groupingMethod, filteredItems]);
 | 
			
		||||
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
@ -55,7 +67,15 @@ export default function Contents(props: Props): JSX.Element {
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
        <p className="flex-shrink-0">{langui.combine_related_contents}:</p>
 | 
			
		||||
        <Switch
 | 
			
		||||
          setState={setCombineRelatedContent}
 | 
			
		||||
          state={combineRelatedContent}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
        <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </SubPanel>
 | 
			
		||||
@ -69,7 +89,8 @@ export default function Contents(props: Props): JSX.Element {
 | 
			
		||||
              {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"
 | 
			
		||||
                  className="text-2xl pb-2 pt-10 first-of-type:pt-0
 | 
			
		||||
                  flex flex-row place-items-center gap-2"
 | 
			
		||||
                >
 | 
			
		||||
                  {name}
 | 
			
		||||
                  <Chip>{`${items.length} ${
 | 
			
		||||
@ -81,22 +102,30 @@ export default function Contents(props: Props): JSX.Element {
 | 
			
		||||
              )}
 | 
			
		||||
              <div
 | 
			
		||||
                key={`items${name}`}
 | 
			
		||||
                className="grid gap-8 items-end grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
 | 
			
		||||
                className="grid gap-8 mobile:gap-4 items-end grid-cols-2
 | 
			
		||||
                desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
 | 
			
		||||
              >
 | 
			
		||||
                {items.map((item) => (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {item.attributes && (
 | 
			
		||||
                      <ThumbnailPreview
 | 
			
		||||
                      <PreviewCard
 | 
			
		||||
                        key={item.id}
 | 
			
		||||
                        href={`/contents/${item.attributes.slug}`}
 | 
			
		||||
                        pre_title={item.attributes.titles?.[0]?.pre_title}
 | 
			
		||||
                        pre_title={item.attributes.translations?.[0]?.pre_title}
 | 
			
		||||
                        title={
 | 
			
		||||
                          item.attributes.titles?.[0]?.title ??
 | 
			
		||||
                          item.attributes.translations?.[0]?.title ??
 | 
			
		||||
                          prettySlug(item.attributes.slug)
 | 
			
		||||
                        }
 | 
			
		||||
                        subtitle={item.attributes.titles?.[0]?.subtitle}
 | 
			
		||||
                        subtitle={item.attributes.translations?.[0]?.subtitle}
 | 
			
		||||
                        thumbnail={item.attributes.thumbnail?.data?.attributes}
 | 
			
		||||
                        thumbnailAspectRatio="3/2"
 | 
			
		||||
                        stackNumber={
 | 
			
		||||
                          combineRelatedContent &&
 | 
			
		||||
                          item.attributes.group?.data?.attributes?.combine
 | 
			
		||||
                            ? item.attributes.group.data.attributes.contents
 | 
			
		||||
                                ?.data.length
 | 
			
		||||
                            : 0
 | 
			
		||||
                        }
 | 
			
		||||
                        topChips={
 | 
			
		||||
                          item.attributes.type?.data?.attributes
 | 
			
		||||
                            ? [
 | 
			
		||||
@ -143,18 +172,18 @@ export async function getStaticProps(
 | 
			
		||||
  });
 | 
			
		||||
  if (!contents.contents) return { notFound: true };
 | 
			
		||||
  contents.contents.data.sort((a, b) => {
 | 
			
		||||
    const titleA = a.attributes?.titles?.[0]
 | 
			
		||||
    const titleA = a.attributes?.translations?.[0]
 | 
			
		||||
      ? prettyinlineTitle(
 | 
			
		||||
          a.attributes.titles[0].pre_title,
 | 
			
		||||
          a.attributes.titles[0].title,
 | 
			
		||||
          a.attributes.titles[0].subtitle
 | 
			
		||||
          a.attributes.translations[0].pre_title,
 | 
			
		||||
          a.attributes.translations[0].title,
 | 
			
		||||
          a.attributes.translations[0].subtitle
 | 
			
		||||
        )
 | 
			
		||||
      : a.attributes?.slug ?? "";
 | 
			
		||||
    const titleB = b.attributes?.titles?.[0]
 | 
			
		||||
    const titleB = b.attributes?.translations?.[0]
 | 
			
		||||
      ? prettyinlineTitle(
 | 
			
		||||
          b.attributes.titles[0].pre_title,
 | 
			
		||||
          b.attributes.titles[0].title,
 | 
			
		||||
          b.attributes.titles[0].subtitle
 | 
			
		||||
          b.attributes.translations[0].pre_title,
 | 
			
		||||
          b.attributes.translations[0].title,
 | 
			
		||||
          b.attributes.translations[0].subtitle
 | 
			
		||||
        )
 | 
			
		||||
      : b.attributes?.slug ?? "";
 | 
			
		||||
    return titleA.localeCompare(titleB);
 | 
			
		||||
@ -172,7 +201,7 @@ export async function getStaticProps(
 | 
			
		||||
function getGroups(
 | 
			
		||||
  langui: AppStaticProps["langui"],
 | 
			
		||||
  groupByType: number,
 | 
			
		||||
  items: Props["contents"]
 | 
			
		||||
  items: Immutable<Props["contents"]>
 | 
			
		||||
): GroupContentItems {
 | 
			
		||||
  switch (groupByType) {
 | 
			
		||||
    case 0: {
 | 
			
		||||
@ -209,15 +238,16 @@ function getGroups(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 1: {
 | 
			
		||||
      const group: GroupContentItems = new Map();
 | 
			
		||||
      const group = new Map();
 | 
			
		||||
      items.map((item) => {
 | 
			
		||||
        const type =
 | 
			
		||||
          item.attributes?.type?.data?.attributes?.titles?.[0]?.title ??
 | 
			
		||||
          prettySlug(item.attributes?.type?.data?.attributes?.slug);
 | 
			
		||||
          item.attributes?.type?.data?.attributes?.slug
 | 
			
		||||
            ? prettySlug(item.attributes.type.data.attributes.slug)
 | 
			
		||||
            : langui.no_type;
 | 
			
		||||
        if (!group.has(type)) group.set(type, []);
 | 
			
		||||
        group.get(type)?.push(item);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return group;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -228,3 +258,19 @@ function getGroups(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function filterContents(
 | 
			
		||||
  combineRelatedContent: boolean,
 | 
			
		||||
  contents: Immutable<Props["contents"]>
 | 
			
		||||
): Immutable<Props["contents"]> {
 | 
			
		||||
  if (combineRelatedContent) {
 | 
			
		||||
    return [...contents].filter(
 | 
			
		||||
      (content) =>
 | 
			
		||||
        !content.attributes?.group?.data?.attributes ||
 | 
			
		||||
        !content.attributes.group.data.attributes.combine ||
 | 
			
		||||
        content.attributes.group.data.attributes.contents?.data[0].id ===
 | 
			
		||||
          content.id
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return contents;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,22 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import {
 | 
			
		||||
  DevGetContentsQuery,
 | 
			
		||||
  Enum_Componentsetstextset_Status,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { DevGetContentsQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  contents: DevGetContentsQuery;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function CheckupContents(props: Props): JSX.Element {
 | 
			
		||||
export default function CheckupContents(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { contents } = props;
 | 
			
		||||
  const testReport = testingContent(contents);
 | 
			
		||||
 | 
			
		||||
@ -38,7 +37,8 @@ export default function CheckupContents(props: Props): JSX.Element {
 | 
			
		||||
      {testReport.lines.map((line, index) => (
 | 
			
		||||
        <div
 | 
			
		||||
          key={index}
 | 
			
		||||
          className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start"
 | 
			
		||||
          className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr]
 | 
			
		||||
          gap-2 items-center mb-2 justify-items-start"
 | 
			
		||||
        >
 | 
			
		||||
          <Button
 | 
			
		||||
            href={line.frontendUrl}
 | 
			
		||||
@ -112,7 +112,7 @@ type ReportLine = {
 | 
			
		||||
  frontendUrl: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
function testingContent(contents: Immutable<Props["contents"]>): Report {
 | 
			
		||||
  const report: Report = {
 | 
			
		||||
    title: "Contents",
 | 
			
		||||
    lines: [],
 | 
			
		||||
@ -163,23 +163,6 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        content.attributes.next_recommended?.data?.id === content.id ||
 | 
			
		||||
        content.attributes.previous_recommended?.data?.id === content.id
 | 
			
		||||
      ) {
 | 
			
		||||
        report.lines.push({
 | 
			
		||||
          subitems: [content.attributes.slug],
 | 
			
		||||
          name: "Self Recommendation",
 | 
			
		||||
          type: "Error",
 | 
			
		||||
          severity: "Very High",
 | 
			
		||||
          description:
 | 
			
		||||
            "The Content is referring to itself as a Next or Previous Recommended.",
 | 
			
		||||
          recommandation: "",
 | 
			
		||||
          backendUrl: backendUrl,
 | 
			
		||||
          frontendUrl: frontendUrl,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!content.attributes.thumbnail?.data?.id) {
 | 
			
		||||
        report.lines.push({
 | 
			
		||||
          subitems: [content.attributes.slug],
 | 
			
		||||
@ -193,7 +176,7 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (content.attributes.titles?.length === 0) {
 | 
			
		||||
      if (content.attributes.translations?.length === 0) {
 | 
			
		||||
        report.lines.push({
 | 
			
		||||
          subitems: [content.attributes.slug],
 | 
			
		||||
          name: "No Titles",
 | 
			
		||||
@ -207,10 +190,10 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
      } else {
 | 
			
		||||
        const titleLanguages: string[] = [];
 | 
			
		||||
 | 
			
		||||
        content.attributes.titles?.map((title, titleIndex) => {
 | 
			
		||||
          if (title && content.attributes) {
 | 
			
		||||
            if (title.language?.data?.id) {
 | 
			
		||||
              if (title.language.data.id in titleLanguages) {
 | 
			
		||||
        content.attributes.translations?.map((translation, titleIndex) => {
 | 
			
		||||
          if (translation && content.attributes) {
 | 
			
		||||
            if (translation.language?.data?.id) {
 | 
			
		||||
              if (translation.language.data.id in titleLanguages) {
 | 
			
		||||
                report.lines.push({
 | 
			
		||||
                  subitems: [
 | 
			
		||||
                    content.attributes.slug,
 | 
			
		||||
@ -225,7 +208,7 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
                  frontendUrl: frontendUrl,
 | 
			
		||||
                });
 | 
			
		||||
              } else {
 | 
			
		||||
                titleLanguages.push(title.language.data.id);
 | 
			
		||||
                titleLanguages.push(translation.language.data.id);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              report.lines.push({
 | 
			
		||||
@ -242,7 +225,7 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
                frontendUrl: frontendUrl,
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
            if (!title.description) {
 | 
			
		||||
            if (!translation.description) {
 | 
			
		||||
              report.lines.push({
 | 
			
		||||
                subitems: [
 | 
			
		||||
                  content.attributes.slug,
 | 
			
		||||
@ -257,229 +240,199 @@ function testingContent(contents: Props["contents"]): Report {
 | 
			
		||||
                frontendUrl: frontendUrl,
 | 
			
		||||
              });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (translation.text_set) {
 | 
			
		||||
              report.lines.push({
 | 
			
		||||
                subitems: [content.attributes.slug],
 | 
			
		||||
                name: "No Text Set",
 | 
			
		||||
                type: "Missing",
 | 
			
		||||
                severity: "Medium",
 | 
			
		||||
                description: "The Content has no Text Set.",
 | 
			
		||||
                recommandation: "",
 | 
			
		||||
                backendUrl: backendUrl,
 | 
			
		||||
                frontendUrl: frontendUrl,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              /*
 | 
			
		||||
               *const textSetLanguages: string[] = [];
 | 
			
		||||
               *if (content.attributes && textSet) {
 | 
			
		||||
               *  if (textSet.language?.data?.id) {
 | 
			
		||||
               *    if (textSet.language.data.id in textSetLanguages) {
 | 
			
		||||
               *      report.lines.push({
 | 
			
		||||
               *        subitems: [
 | 
			
		||||
               *          content.attributes.slug,
 | 
			
		||||
               *          `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *        ],
 | 
			
		||||
               *        name: "Duplicate Language",
 | 
			
		||||
               *        type: "Error",
 | 
			
		||||
               *        severity: "High",
 | 
			
		||||
               *        description: "",
 | 
			
		||||
               *        recommandation: "",
 | 
			
		||||
               *        backendUrl: backendUrl,
 | 
			
		||||
               *        frontendUrl: frontendUrl,
 | 
			
		||||
               *      });
 | 
			
		||||
               *    } else {
 | 
			
		||||
               *      textSetLanguages.push(textSet.language.data.id);
 | 
			
		||||
               *    }
 | 
			
		||||
               *  } else {
 | 
			
		||||
               *    report.lines.push({
 | 
			
		||||
               *      subitems: [
 | 
			
		||||
               *        content.attributes.slug,
 | 
			
		||||
               *        `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *      ],
 | 
			
		||||
               *      name: "No Language",
 | 
			
		||||
               *      type: "Error",
 | 
			
		||||
               *      severity: "Very High",
 | 
			
		||||
               *      description: "",
 | 
			
		||||
               *      recommandation: "",
 | 
			
		||||
               *      backendUrl: backendUrl,
 | 
			
		||||
               *      frontendUrl: frontendUrl,
 | 
			
		||||
               *    });
 | 
			
		||||
               *  }
 | 
			
		||||
               *
 | 
			
		||||
               *  if (!textSet.source_language?.data?.id) {
 | 
			
		||||
               *    report.lines.push({
 | 
			
		||||
               *      subitems: [
 | 
			
		||||
               *        content.attributes.slug,
 | 
			
		||||
               *        `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *      ],
 | 
			
		||||
               *      name: "No Source Language",
 | 
			
		||||
               *      type: "Error",
 | 
			
		||||
               *      severity: "High",
 | 
			
		||||
               *      description: "",
 | 
			
		||||
               *      recommandation: "",
 | 
			
		||||
               *      backendUrl: backendUrl,
 | 
			
		||||
               *      frontendUrl: frontendUrl,
 | 
			
		||||
               *    });
 | 
			
		||||
               *  }
 | 
			
		||||
               *
 | 
			
		||||
               *  if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
 | 
			
		||||
               *    report.lines.push({
 | 
			
		||||
               *      subitems: [
 | 
			
		||||
               *        content.attributes.slug,
 | 
			
		||||
               *        `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *      ],
 | 
			
		||||
               *      name: "Not Done Status",
 | 
			
		||||
               *      type: "Improvement",
 | 
			
		||||
               *      severity: "Low",
 | 
			
		||||
               *      description: "",
 | 
			
		||||
               *      recommandation: "",
 | 
			
		||||
               *      backendUrl: backendUrl,
 | 
			
		||||
               *      frontendUrl: frontendUrl,
 | 
			
		||||
               *    });
 | 
			
		||||
               *  }
 | 
			
		||||
               *
 | 
			
		||||
               *  if (!textSet.text || textSet.text.length < 10) {
 | 
			
		||||
               *    report.lines.push({
 | 
			
		||||
               *      subitems: [
 | 
			
		||||
               *        content.attributes.slug,
 | 
			
		||||
               *        `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *      ],
 | 
			
		||||
               *      name: "No Text",
 | 
			
		||||
               *      type: "Missing",
 | 
			
		||||
               *      severity: "Medium",
 | 
			
		||||
               *      description: "",
 | 
			
		||||
               *      recommandation: "",
 | 
			
		||||
               *      backendUrl: backendUrl,
 | 
			
		||||
               *      frontendUrl: frontendUrl,
 | 
			
		||||
               *    });
 | 
			
		||||
               *  }
 | 
			
		||||
               *
 | 
			
		||||
               *  if (
 | 
			
		||||
               *    textSet.source_language?.data?.id ===
 | 
			
		||||
               *    textSet.language?.data?.id
 | 
			
		||||
               *  ) {
 | 
			
		||||
               *    if (textSet.transcribers?.data.length === 0) {
 | 
			
		||||
               *      report.lines.push({
 | 
			
		||||
               *        subitems: [
 | 
			
		||||
               *          content.attributes.slug,
 | 
			
		||||
               *          `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *        ],
 | 
			
		||||
               *        name: "No Transcribers",
 | 
			
		||||
               *        type: "Missing",
 | 
			
		||||
               *        severity: "High",
 | 
			
		||||
               *        description:
 | 
			
		||||
               *          "The Content is a Transcription but doesn't credit any Transcribers.",
 | 
			
		||||
               *        recommandation: "Add the appropriate Transcribers.",
 | 
			
		||||
               *        backendUrl: backendUrl,
 | 
			
		||||
               *        frontendUrl: frontendUrl,
 | 
			
		||||
               *      });
 | 
			
		||||
               *    }
 | 
			
		||||
               *    if (
 | 
			
		||||
               *      textSet.translators?.data &&
 | 
			
		||||
               *      textSet.translators.data.length > 0
 | 
			
		||||
               *    ) {
 | 
			
		||||
               *      report.lines.push({
 | 
			
		||||
               *        subitems: [
 | 
			
		||||
               *          content.attributes.slug,
 | 
			
		||||
               *          `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *        ],
 | 
			
		||||
               *        name: "Credited Translators",
 | 
			
		||||
               *        type: "Error",
 | 
			
		||||
               *        severity: "High",
 | 
			
		||||
               *        description:
 | 
			
		||||
               *          "The Content is a Transcription but credits one or more Translators.",
 | 
			
		||||
               *        recommandation:
 | 
			
		||||
               *          "If appropriate, create a Translation Text Set with the Translator credited there.",
 | 
			
		||||
               *        backendUrl: backendUrl,
 | 
			
		||||
               *        frontendUrl: frontendUrl,
 | 
			
		||||
               *      });
 | 
			
		||||
               *    }
 | 
			
		||||
               *  } else {
 | 
			
		||||
               *    if (textSet.translators?.data.length === 0) {
 | 
			
		||||
               *      report.lines.push({
 | 
			
		||||
               *        subitems: [
 | 
			
		||||
               *          content.attributes.slug,
 | 
			
		||||
               *          `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *        ],
 | 
			
		||||
               *        name: "No Translators",
 | 
			
		||||
               *        type: "Missing",
 | 
			
		||||
               *        severity: "High",
 | 
			
		||||
               *        description:
 | 
			
		||||
               *          "The Content is a Transcription but doesn't credit any Translators.",
 | 
			
		||||
               *        recommandation: "Add the appropriate Translators.",
 | 
			
		||||
               *        backendUrl: backendUrl,
 | 
			
		||||
               *        frontendUrl: frontendUrl,
 | 
			
		||||
               *      });
 | 
			
		||||
               *    }
 | 
			
		||||
               *    if (
 | 
			
		||||
               *      textSet.transcribers?.data &&
 | 
			
		||||
               *      textSet.transcribers.data.length > 0
 | 
			
		||||
               *    ) {
 | 
			
		||||
               *      report.lines.push({
 | 
			
		||||
               *        subitems: [
 | 
			
		||||
               *          content.attributes.slug,
 | 
			
		||||
               *          `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
               *        ],
 | 
			
		||||
               *        name: "Credited Transcribers",
 | 
			
		||||
               *        type: "Error",
 | 
			
		||||
               *        severity: "High",
 | 
			
		||||
               *        description:
 | 
			
		||||
               *          "The Content is a Translation but credits one or more Transcribers.",
 | 
			
		||||
               *        recommandation:
 | 
			
		||||
               *          "If appropriate, create a Transcription Text Set with the Transcribers credited there.",
 | 
			
		||||
               *        backendUrl: backendUrl,
 | 
			
		||||
               *        frontendUrl: frontendUrl,
 | 
			
		||||
               *      });
 | 
			
		||||
               *    }
 | 
			
		||||
               *  }
 | 
			
		||||
               *}
 | 
			
		||||
               */
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            report.lines.push({
 | 
			
		||||
              subitems: [content.attributes.slug],
 | 
			
		||||
              name: "No Sets",
 | 
			
		||||
              type: "Missing",
 | 
			
		||||
              severity: "Medium",
 | 
			
		||||
              description: "The Content has no Sets.",
 | 
			
		||||
              recommandation: "",
 | 
			
		||||
              backendUrl: backendUrl,
 | 
			
		||||
              frontendUrl: frontendUrl,
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (
 | 
			
		||||
        content.attributes.text_set?.length === 0 &&
 | 
			
		||||
        content.attributes.audio_set?.length === 0 &&
 | 
			
		||||
        content.attributes.video_set?.length === 0
 | 
			
		||||
      ) {
 | 
			
		||||
        report.lines.push({
 | 
			
		||||
          subitems: [content.attributes.slug],
 | 
			
		||||
          name: "No Sets",
 | 
			
		||||
          type: "Missing",
 | 
			
		||||
          severity: "Medium",
 | 
			
		||||
          description: "The Content has no Sets.",
 | 
			
		||||
          recommandation: "",
 | 
			
		||||
          backendUrl: backendUrl,
 | 
			
		||||
          frontendUrl: frontendUrl,
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        if (content.attributes.video_set?.length === 0) {
 | 
			
		||||
          report.lines.push({
 | 
			
		||||
            subitems: [content.attributes.slug],
 | 
			
		||||
            name: "No Video Sets",
 | 
			
		||||
            type: "Missing",
 | 
			
		||||
            severity: "Very Low",
 | 
			
		||||
            description: "The Content has no Video Sets.",
 | 
			
		||||
            recommandation: "",
 | 
			
		||||
            backendUrl: backendUrl,
 | 
			
		||||
            frontendUrl: frontendUrl,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        if (content.attributes.audio_set?.length === 0) {
 | 
			
		||||
          report.lines.push({
 | 
			
		||||
            subitems: [content.attributes.slug],
 | 
			
		||||
            name: "No Audio Sets",
 | 
			
		||||
            type: "Missing",
 | 
			
		||||
            severity: "Very Low",
 | 
			
		||||
            description: "The Content has no Audio Sets.",
 | 
			
		||||
            recommandation: "",
 | 
			
		||||
            backendUrl: backendUrl,
 | 
			
		||||
            frontendUrl: frontendUrl,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        if (content.attributes.text_set?.length === 0) {
 | 
			
		||||
          report.lines.push({
 | 
			
		||||
            subitems: [content.attributes.slug],
 | 
			
		||||
            name: "No Text Set",
 | 
			
		||||
            type: "Missing",
 | 
			
		||||
            severity: "Medium",
 | 
			
		||||
            description: "The Content has no Text Set.",
 | 
			
		||||
            recommandation: "",
 | 
			
		||||
            backendUrl: backendUrl,
 | 
			
		||||
            frontendUrl: frontendUrl,
 | 
			
		||||
          });
 | 
			
		||||
        } else {
 | 
			
		||||
          const textSetLanguages: string[] = [];
 | 
			
		||||
 | 
			
		||||
          content.attributes.text_set?.map((textSet, textSetIndex) => {
 | 
			
		||||
            if (content.attributes && textSet) {
 | 
			
		||||
              if (textSet.language?.data?.id) {
 | 
			
		||||
                if (textSet.language.data.id in textSetLanguages) {
 | 
			
		||||
                  report.lines.push({
 | 
			
		||||
                    subitems: [
 | 
			
		||||
                      content.attributes.slug,
 | 
			
		||||
                      `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                    ],
 | 
			
		||||
                    name: "Duplicate Language",
 | 
			
		||||
                    type: "Error",
 | 
			
		||||
                    severity: "High",
 | 
			
		||||
                    description: "",
 | 
			
		||||
                    recommandation: "",
 | 
			
		||||
                    backendUrl: backendUrl,
 | 
			
		||||
                    frontendUrl: frontendUrl,
 | 
			
		||||
                  });
 | 
			
		||||
                } else {
 | 
			
		||||
                  textSetLanguages.push(textSet.language.data.id);
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                report.lines.push({
 | 
			
		||||
                  subitems: [
 | 
			
		||||
                    content.attributes.slug,
 | 
			
		||||
                    `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                  ],
 | 
			
		||||
                  name: "No Language",
 | 
			
		||||
                  type: "Error",
 | 
			
		||||
                  severity: "Very High",
 | 
			
		||||
                  description: "",
 | 
			
		||||
                  recommandation: "",
 | 
			
		||||
                  backendUrl: backendUrl,
 | 
			
		||||
                  frontendUrl: frontendUrl,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (!textSet.source_language?.data?.id) {
 | 
			
		||||
                report.lines.push({
 | 
			
		||||
                  subitems: [
 | 
			
		||||
                    content.attributes.slug,
 | 
			
		||||
                    `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                  ],
 | 
			
		||||
                  name: "No Source Language",
 | 
			
		||||
                  type: "Error",
 | 
			
		||||
                  severity: "High",
 | 
			
		||||
                  description: "",
 | 
			
		||||
                  recommandation: "",
 | 
			
		||||
                  backendUrl: backendUrl,
 | 
			
		||||
                  frontendUrl: frontendUrl,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
 | 
			
		||||
                report.lines.push({
 | 
			
		||||
                  subitems: [
 | 
			
		||||
                    content.attributes.slug,
 | 
			
		||||
                    `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                  ],
 | 
			
		||||
                  name: "Not Done Status",
 | 
			
		||||
                  type: "Improvement",
 | 
			
		||||
                  severity: "Low",
 | 
			
		||||
                  description: "",
 | 
			
		||||
                  recommandation: "",
 | 
			
		||||
                  backendUrl: backendUrl,
 | 
			
		||||
                  frontendUrl: frontendUrl,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (!textSet.text || textSet.text.length < 10) {
 | 
			
		||||
                report.lines.push({
 | 
			
		||||
                  subitems: [
 | 
			
		||||
                    content.attributes.slug,
 | 
			
		||||
                    `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                  ],
 | 
			
		||||
                  name: "No Text",
 | 
			
		||||
                  type: "Missing",
 | 
			
		||||
                  severity: "Medium",
 | 
			
		||||
                  description: "",
 | 
			
		||||
                  recommandation: "",
 | 
			
		||||
                  backendUrl: backendUrl,
 | 
			
		||||
                  frontendUrl: frontendUrl,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              if (
 | 
			
		||||
                textSet.source_language?.data?.id === textSet.language?.data?.id
 | 
			
		||||
              ) {
 | 
			
		||||
                if (textSet.transcribers?.data.length === 0) {
 | 
			
		||||
                  report.lines.push({
 | 
			
		||||
                    subitems: [
 | 
			
		||||
                      content.attributes.slug,
 | 
			
		||||
                      `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                    ],
 | 
			
		||||
                    name: "No Transcribers",
 | 
			
		||||
                    type: "Missing",
 | 
			
		||||
                    severity: "High",
 | 
			
		||||
                    description:
 | 
			
		||||
                      "The Content is a Transcription but doesn't credit any Transcribers.",
 | 
			
		||||
                    recommandation: "Add the appropriate Transcribers.",
 | 
			
		||||
                    backendUrl: backendUrl,
 | 
			
		||||
                    frontendUrl: frontendUrl,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
                if (
 | 
			
		||||
                  textSet.translators?.data &&
 | 
			
		||||
                  textSet.translators.data.length > 0
 | 
			
		||||
                ) {
 | 
			
		||||
                  report.lines.push({
 | 
			
		||||
                    subitems: [
 | 
			
		||||
                      content.attributes.slug,
 | 
			
		||||
                      `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                    ],
 | 
			
		||||
                    name: "Credited Translators",
 | 
			
		||||
                    type: "Error",
 | 
			
		||||
                    severity: "High",
 | 
			
		||||
                    description:
 | 
			
		||||
                      "The Content is a Transcription but credits one or more Translators.",
 | 
			
		||||
                    recommandation:
 | 
			
		||||
                      "If appropriate, create a Translation Text Set with the Translator credited there.",
 | 
			
		||||
                    backendUrl: backendUrl,
 | 
			
		||||
                    frontendUrl: frontendUrl,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                if (textSet.translators?.data.length === 0) {
 | 
			
		||||
                  report.lines.push({
 | 
			
		||||
                    subitems: [
 | 
			
		||||
                      content.attributes.slug,
 | 
			
		||||
                      `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                    ],
 | 
			
		||||
                    name: "No Translators",
 | 
			
		||||
                    type: "Missing",
 | 
			
		||||
                    severity: "High",
 | 
			
		||||
                    description:
 | 
			
		||||
                      "The Content is a Transcription but doesn't credit any Translators.",
 | 
			
		||||
                    recommandation: "Add the appropriate Translators.",
 | 
			
		||||
                    backendUrl: backendUrl,
 | 
			
		||||
                    frontendUrl: frontendUrl,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
                if (
 | 
			
		||||
                  textSet.transcribers?.data &&
 | 
			
		||||
                  textSet.transcribers.data.length > 0
 | 
			
		||||
                ) {
 | 
			
		||||
                  report.lines.push({
 | 
			
		||||
                    subitems: [
 | 
			
		||||
                      content.attributes.slug,
 | 
			
		||||
                      `TextSet ${textSetIndex.toString()}`,
 | 
			
		||||
                    ],
 | 
			
		||||
                    name: "Credited Transcribers",
 | 
			
		||||
                    type: "Error",
 | 
			
		||||
                    severity: "High",
 | 
			
		||||
                    description:
 | 
			
		||||
                      "The Content is a Translation but credits one or more Transcribers.",
 | 
			
		||||
                    recommandation:
 | 
			
		||||
                      "If appropriate, create a Transcription Text Set with the Transcribers credited there.",
 | 
			
		||||
                    backendUrl: backendUrl,
 | 
			
		||||
                    frontendUrl: frontendUrl,
 | 
			
		||||
                  });
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return report;
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,27 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import {
 | 
			
		||||
  DevGetLibraryItemsQuery,
 | 
			
		||||
  Enum_Componentcollectionscomponentlibraryimages_Status,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  libraryItems: DevGetLibraryItemsQuery;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function CheckupLibraryItems(props: Props): JSX.Element {
 | 
			
		||||
export default function CheckupLibraryItems(
 | 
			
		||||
  props: Immutable<Props>
 | 
			
		||||
): JSX.Element {
 | 
			
		||||
  const { libraryItems } = props;
 | 
			
		||||
  const testReport = testingLibraryItem(libraryItems);
 | 
			
		||||
 | 
			
		||||
@ -38,7 +42,8 @@ export default function CheckupLibraryItems(props: Props): JSX.Element {
 | 
			
		||||
      {testReport.lines.map((line, index) => (
 | 
			
		||||
        <div
 | 
			
		||||
          key={index}
 | 
			
		||||
          className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] gap-2 items-center mb-2 justify-items-start"
 | 
			
		||||
          className="grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr]
 | 
			
		||||
          gap-2 items-center mb-2 justify-items-start"
 | 
			
		||||
        >
 | 
			
		||||
          <Button
 | 
			
		||||
            href={line.frontendUrl}
 | 
			
		||||
@ -113,7 +118,9 @@ type ReportLine = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
function testingLibraryItem(libraryItems: Props["libraryItems"]): Report {
 | 
			
		||||
function testingLibraryItem(
 | 
			
		||||
  libraryItems: Immutable<Props["libraryItems"]>
 | 
			
		||||
): Report {
 | 
			
		||||
  const report: Report = {
 | 
			
		||||
    title: "Contents",
 | 
			
		||||
    lines: [],
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,21 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import Markdawn from "components/Markdown/Markdawn";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { Markdawn } from "components/Markdown/Markdawn";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import Popup from "components/Popup";
 | 
			
		||||
import ToolTip from "components/ToolTip";
 | 
			
		||||
import { Popup } from "components/Popup";
 | 
			
		||||
import { ToolTip } from "components/ToolTip";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { useCallback, useState } from "react";
 | 
			
		||||
import TurndownService from "turndown";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function Editor(props: Props): JSX.Element {
 | 
			
		||||
export default function Editor(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const handleInput = useCallback((text: string) => {
 | 
			
		||||
    setMarkdown(text);
 | 
			
		||||
  }, []);
 | 
			
		||||
@ -337,7 +339,8 @@ export default function Editor(props: Props): JSX.Element {
 | 
			
		||||
              const textarea = event.target as HTMLTextAreaElement;
 | 
			
		||||
              handleInput(textarea.value);
 | 
			
		||||
            }}
 | 
			
		||||
            className="bg-mid !bg-opacity-40 rounded-xl outline-none p-8 w-full text-black font-monospace h-[70vh]"
 | 
			
		||||
            className="bg-mid !bg-opacity-40 rounded-xl
 | 
			
		||||
            outline-none p-8 w-full text-black font-monospace h-[70vh]"
 | 
			
		||||
            value={markdown}
 | 
			
		||||
            title="Input textarea"
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function Gallery(props: Props): JSX.Element {
 | 
			
		||||
export default function Gallery(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <iframe
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,11 @@
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Home(props: Props): JSX.Element {
 | 
			
		||||
export default function Home(props: Immutable<PostStaticProps>): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <PostPage
 | 
			
		||||
@ -17,7 +15,11 @@ export default function Home(props: Props): JSX.Element {
 | 
			
		||||
      post={post}
 | 
			
		||||
      prependBody={
 | 
			
		||||
        <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" />
 | 
			
		||||
          <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’s Library</h1>
 | 
			
		||||
          <h2 className="text-xl -mt-5">
 | 
			
		||||
            Discover • Analyze • Translate • Archive
 | 
			
		||||
@ -30,21 +32,4 @@ export default function Home(props: Props): JSX.Element {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = "home";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
export const getStaticProps = getPostStaticProps("home");
 | 
			
		||||
 | 
			
		||||
@ -1,57 +1,59 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Img, { getAssetURL, ImageQuality } from "components/Img";
 | 
			
		||||
import Button from "components/Inputs/Button";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import InsetBox from "components/InsetBox";
 | 
			
		||||
import ContentLine from "components/Library/ContentLine";
 | 
			
		||||
import LightBox from "components/LightBox";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Img } from "components/Img";
 | 
			
		||||
import { Button } from "components/Inputs/Button";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { InsetBox } from "components/InsetBox";
 | 
			
		||||
import { ContentLine } from "components/Library/ContentLine";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import {
 | 
			
		||||
  Enum_Componentmetadatabooks_Binding_Type,
 | 
			
		||||
  Enum_Componentmetadatabooks_Page_Order,
 | 
			
		||||
  GetLibraryItemQuery,
 | 
			
		||||
} from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import {
 | 
			
		||||
  convertMmToInch,
 | 
			
		||||
  prettyDate,
 | 
			
		||||
  prettyinlineTitle,
 | 
			
		||||
  prettyItemSubType,
 | 
			
		||||
  prettyItemType,
 | 
			
		||||
  prettyPrice,
 | 
			
		||||
  prettyURL,
 | 
			
		||||
  sortContent,
 | 
			
		||||
} from "queries/helpers";
 | 
			
		||||
} from "helpers/formatters";
 | 
			
		||||
import { getAssetURL, ImageQuality } from "helpers/img";
 | 
			
		||||
import { convertMmToInch } from "helpers/numbers";
 | 
			
		||||
import { sortContent } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useLightBox } from "hooks/useLightBox";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  item: Exclude<
 | 
			
		||||
    GetLibraryItemQuery["libraryItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  item: NonNullable<
 | 
			
		||||
    GetLibraryItemQuery["libraryItems"]
 | 
			
		||||
  >["data"][number]["attributes"];
 | 
			
		||||
  itemId: Exclude<
 | 
			
		||||
    GetLibraryItemQuery["libraryItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  itemId: NonNullable<
 | 
			
		||||
    GetLibraryItemQuery["libraryItems"]
 | 
			
		||||
  >["data"][number]["id"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { item, langui, currencies } = props;
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
 | 
			
		||||
@ -61,10 +63,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
  sortContent(item?.contents);
 | 
			
		||||
 | 
			
		||||
  const [lightboxOpen, setLightboxOpen] = useState(false);
 | 
			
		||||
  const [lightboxImages, setLightboxImages] = useState([""]);
 | 
			
		||||
  const [lightboxIndex, setLightboxIndex] = useState(0);
 | 
			
		||||
 | 
			
		||||
  const [openLightBox, LightBox] = useLightBox();
 | 
			
		||||
  const [keepInfoVisible, setKeepInfoVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  let displayOpenScans = false;
 | 
			
		||||
@ -134,13 +133,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <ContentPanel width={ContentPanelWidthSizes.large}>
 | 
			
		||||
      <LightBox
 | 
			
		||||
        state={lightboxOpen}
 | 
			
		||||
        setState={setLightboxOpen}
 | 
			
		||||
        images={lightboxImages}
 | 
			
		||||
        index={lightboxIndex}
 | 
			
		||||
        setIndex={setLightboxIndex}
 | 
			
		||||
      />
 | 
			
		||||
      <LightBox />
 | 
			
		||||
 | 
			
		||||
      <ReturnButton
 | 
			
		||||
        href="/library/"
 | 
			
		||||
@ -151,17 +144,16 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
      />
 | 
			
		||||
      <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"
 | 
			
		||||
          className="drop-shadow-shade-xl w-full h-[50vh]
 | 
			
		||||
          mobile:h-[60vh] desktop:mb-16 relative cursor-pointer"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            if (item?.thumbnail?.data?.attributes) {
 | 
			
		||||
              setLightboxOpen(true);
 | 
			
		||||
              setLightboxImages([
 | 
			
		||||
              openLightBox([
 | 
			
		||||
                getAssetURL(
 | 
			
		||||
                  item.thumbnail.data.attributes.url,
 | 
			
		||||
                  ImageQuality.Large
 | 
			
		||||
                ),
 | 
			
		||||
              ]);
 | 
			
		||||
              setLightboxIndex(0);
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
@ -169,9 +161,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
            <Img
 | 
			
		||||
              image={item.thumbnail.data.attributes}
 | 
			
		||||
              quality={ImageQuality.Large}
 | 
			
		||||
              layout="fill"
 | 
			
		||||
              objectFit="contain"
 | 
			
		||||
              priority
 | 
			
		||||
              className="w-full h-full object-contain"
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div className="w-full aspect-[21/29.7] bg-light rounded-xl"></div>
 | 
			
		||||
@ -212,7 +202,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
              <>
 | 
			
		||||
                {item?.urls && item.urls.length ? (
 | 
			
		||||
                  <div className="flex flex-row place-items-center gap-3">
 | 
			
		||||
                    <p>Available at</p>
 | 
			
		||||
                    <p>{langui.available_at}</p>
 | 
			
		||||
                    {item.urls.map((url) => (
 | 
			
		||||
                      <>
 | 
			
		||||
                        {url?.url && (
 | 
			
		||||
@ -228,7 +218,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </div>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <p>This item is not for sale or is no longer available</p>
 | 
			
		||||
                  <p>{langui.item_not_available}</p>
 | 
			
		||||
                )}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
@ -238,13 +228,17 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
        {item?.gallery && 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))]">
 | 
			
		||||
            <div
 | 
			
		||||
              className="grid w-full gap-8 items-end
 | 
			
		||||
              grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
 | 
			
		||||
            >
 | 
			
		||||
              {item.gallery.data.map((galleryItem, index) => (
 | 
			
		||||
                <>
 | 
			
		||||
                  {galleryItem.attributes && (
 | 
			
		||||
                    <div
 | 
			
		||||
                      key={galleryItem.id}
 | 
			
		||||
                      className="relative aspect-square hover:scale-[1.02] transition-transform cursor-pointer"
 | 
			
		||||
                      className="relative aspect-square hover:scale-[1.02]
 | 
			
		||||
                      transition-transform cursor-pointer"
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        if (item.gallery?.data) {
 | 
			
		||||
                          const images: string[] = [];
 | 
			
		||||
@ -257,18 +251,14 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
                                )
 | 
			
		||||
                              );
 | 
			
		||||
                          });
 | 
			
		||||
                          setLightboxOpen(true);
 | 
			
		||||
                          setLightboxImages(images);
 | 
			
		||||
                          setLightboxIndex(index);
 | 
			
		||||
                          openLightBox(images, index);
 | 
			
		||||
                        }
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <div className="bg-light absolute inset-0 rounded-lg drop-shadow-shade-md"></div>
 | 
			
		||||
                      <Img
 | 
			
		||||
                        className="rounded-lg"
 | 
			
		||||
                        className="bg-light rounded-lg drop-shadow-shade-md
 | 
			
		||||
                        w-full h-full object-cover"
 | 
			
		||||
                        image={galleryItem.attributes}
 | 
			
		||||
                        layout="fill"
 | 
			
		||||
                        objectFit="cover"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
@ -425,14 +415,17 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
            </h2>
 | 
			
		||||
 | 
			
		||||
            <div className="-mt-6 mb-8 flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
              <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
              <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
              <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="grid gap-8 items-end mobile:grid-cols-2 grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] w-full">
 | 
			
		||||
            <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) => (
 | 
			
		||||
                <>
 | 
			
		||||
                  {subitem.attributes && (
 | 
			
		||||
                    <ThumbnailPreview
 | 
			
		||||
                    <PreviewCard
 | 
			
		||||
                      key={subitem.id}
 | 
			
		||||
                      href={`/library/${subitem.attributes.slug}`}
 | 
			
		||||
                      title={subitem.attributes.title}
 | 
			
		||||
 | 
			
		||||
@ -1,47 +1,46 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import ScanSet from "components/Library/ScanSet";
 | 
			
		||||
import ScanSetCover from "components/Library/ScanSetCover";
 | 
			
		||||
import LightBox from "components/LightBox";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { ScanSet } from "components/Library/ScanSet";
 | 
			
		||||
import { ScanSetCover } from "components/Library/ScanSetCover";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import {
 | 
			
		||||
  ReturnButton,
 | 
			
		||||
  ReturnButtonType,
 | 
			
		||||
} from "components/PanelComponents/ReturnButton";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { GetLibraryItemScansQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
 | 
			
		||||
import { sortContent } from "helpers/others";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { useLightBox } from "hooks/useLightBox";
 | 
			
		||||
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: Exclude<
 | 
			
		||||
    GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  item: NonNullable<
 | 
			
		||||
    GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
  >["data"][number]["attributes"];
 | 
			
		||||
  itemId: Exclude<
 | 
			
		||||
    GetLibraryItemScansQuery["libraryItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  itemId: NonNullable<
 | 
			
		||||
    GetLibraryItemScansQuery["libraryItems"]
 | 
			
		||||
  >["data"][number]["id"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { item, langui, languages } = props;
 | 
			
		||||
  const appLayout = useAppLayout();
 | 
			
		||||
 | 
			
		||||
  sortContent(item?.contents);
 | 
			
		||||
 | 
			
		||||
  const [lightboxOpen, setLightboxOpen] = useState(false);
 | 
			
		||||
  const [lightboxImages, setLightboxImages] = useState([""]);
 | 
			
		||||
  const [lightboxIndex, setLightboxIndex] = useState(0);
 | 
			
		||||
  const [openLightBox, LightBox] = useLightBox();
 | 
			
		||||
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
@ -61,7 +60,9 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
          subtitle={
 | 
			
		||||
            content.attributes?.range[0]?.__typename ===
 | 
			
		||||
            "ComponentRangePageRange"
 | 
			
		||||
              ? `${content.attributes.range[0].starting_page} → ${content.attributes.range[0].ending_page}`
 | 
			
		||||
              ? `${content.attributes.range[0].starting_page}` +
 | 
			
		||||
                `→` +
 | 
			
		||||
                `${content.attributes.range[0].ending_page}`
 | 
			
		||||
              : undefined
 | 
			
		||||
          }
 | 
			
		||||
          onClick={() => appLayout.setSubPanelOpen(false)}
 | 
			
		||||
@ -73,13 +74,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
  const contentPanel = (
 | 
			
		||||
    <ContentPanel width={ContentPanelWidthSizes.large}>
 | 
			
		||||
      <LightBox
 | 
			
		||||
        state={lightboxOpen}
 | 
			
		||||
        setState={setLightboxOpen}
 | 
			
		||||
        images={lightboxImages}
 | 
			
		||||
        index={lightboxIndex}
 | 
			
		||||
        setIndex={setLightboxIndex}
 | 
			
		||||
      />
 | 
			
		||||
      <LightBox />
 | 
			
		||||
 | 
			
		||||
      <ReturnButton
 | 
			
		||||
        href={`/library/${item?.slug}`}
 | 
			
		||||
@ -92,9 +87,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
      {item?.images && (
 | 
			
		||||
        <ScanSetCover
 | 
			
		||||
          images={item.images}
 | 
			
		||||
          setLightboxImages={setLightboxImages}
 | 
			
		||||
          setLightboxIndex={setLightboxIndex}
 | 
			
		||||
          setLightboxOpen={setLightboxOpen}
 | 
			
		||||
          openLightBox={openLightBox}
 | 
			
		||||
          languages={languages}
 | 
			
		||||
          langui={langui}
 | 
			
		||||
        />
 | 
			
		||||
@ -106,9 +99,7 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
            <ScanSet
 | 
			
		||||
              key={content.id}
 | 
			
		||||
              scanSet={content.attributes.scan_set}
 | 
			
		||||
              setLightboxImages={setLightboxImages}
 | 
			
		||||
              setLightboxIndex={setLightboxIndex}
 | 
			
		||||
              setLightboxOpen={setLightboxOpen}
 | 
			
		||||
              openLightBox={openLightBox}
 | 
			
		||||
              slug={content.attributes.slug}
 | 
			
		||||
              title={prettySlug(content.attributes.slug, item.slug)}
 | 
			
		||||
              languages={languages}
 | 
			
		||||
 | 
			
		||||
@ -1,35 +1,34 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Chip from "components/Chip";
 | 
			
		||||
import Select from "components/Inputs/Select";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Chip } from "components/Chip";
 | 
			
		||||
import { Select } from "components/Inputs/Select";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { GetLibraryItemsPreviewQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import {
 | 
			
		||||
  convertPrice,
 | 
			
		||||
  prettyDate,
 | 
			
		||||
  prettyinlineTitle,
 | 
			
		||||
  prettyItemSubType,
 | 
			
		||||
} from "queries/helpers";
 | 
			
		||||
} from "helpers/formatters";
 | 
			
		||||
import { convertPrice } from "helpers/numbers";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  items: Exclude<
 | 
			
		||||
    GetLibraryItemsPreviewQuery["libraryItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"];
 | 
			
		||||
  items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GroupLibraryItems = Map<string, Props["items"]>;
 | 
			
		||||
type GroupLibraryItems = Map<string, Immutable<Props["items"]>>;
 | 
			
		||||
 | 
			
		||||
export default function Library(props: Props): JSX.Element {
 | 
			
		||||
export default function Library(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui, items: libraryItems, currencies } = props;
 | 
			
		||||
 | 
			
		||||
  const [showSubitems, setShowSubitems] = useState<boolean>(false);
 | 
			
		||||
@ -39,7 +38,7 @@ export default function Library(props: Props): JSX.Element {
 | 
			
		||||
  const [groupingMethod, setGroupingMethod] = useState<number>(-1);
 | 
			
		||||
  const [keepInfoVisible, setKeepInfoVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const [filteredItems, setFilteredItems] = useState<Props["items"]>(
 | 
			
		||||
  const [filteredItems, setFilteredItems] = useState(
 | 
			
		||||
    filterItems(
 | 
			
		||||
      showSubitems,
 | 
			
		||||
      showPrimaryItems,
 | 
			
		||||
@ -48,11 +47,11 @@ export default function Library(props: Props): JSX.Element {
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [sortedItems, setSortedItem] = useState<Props["items"]>(
 | 
			
		||||
  const [sortedItems, setSortedItem] = useState(
 | 
			
		||||
    sortBy(groupingMethod, filteredItems, currencies)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const [groups, setGroups] = useState<GroupLibraryItems>(
 | 
			
		||||
  const [groups, setGroups] = useState(
 | 
			
		||||
    getGroups(langui, groupingMethod, sortedItems)
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -128,7 +127,7 @@ export default function Library(props: Props): JSX.Element {
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
        <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
        <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </SubPanel>
 | 
			
		||||
@ -142,7 +141,8 @@ export default function Library(props: Props): JSX.Element {
 | 
			
		||||
              {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"
 | 
			
		||||
                  className="text-2xl pb-2 pt-10 first-of-type:pt-0
 | 
			
		||||
                  flex flex-row place-items-center gap-2"
 | 
			
		||||
                >
 | 
			
		||||
                  {name}
 | 
			
		||||
                  <Chip>{`${items.length} ${
 | 
			
		||||
@ -154,12 +154,14 @@ export default function Library(props: Props): JSX.Element {
 | 
			
		||||
              )}
 | 
			
		||||
              <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"
 | 
			
		||||
                className="grid gap-8 mobile:gap-4 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) => (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {item.attributes && (
 | 
			
		||||
                      <ThumbnailPreview
 | 
			
		||||
                      <PreviewCard
 | 
			
		||||
                        key={item.id}
 | 
			
		||||
                        href={`/library/${item.attributes.slug}`}
 | 
			
		||||
                        title={item.attributes.title}
 | 
			
		||||
@ -224,7 +226,7 @@ export async function getStaticProps(
 | 
			
		||||
function getGroups(
 | 
			
		||||
  langui: AppStaticProps["langui"],
 | 
			
		||||
  groupByType: number,
 | 
			
		||||
  items: Props["items"]
 | 
			
		||||
  items: Immutable<Props["items"]>
 | 
			
		||||
): GroupLibraryItems {
 | 
			
		||||
  switch (groupByType) {
 | 
			
		||||
    case 0: {
 | 
			
		||||
@ -262,7 +264,7 @@ function getGroups(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case 1: {
 | 
			
		||||
      const group: GroupLibraryItems = new Map();
 | 
			
		||||
      const group = new Map();
 | 
			
		||||
      group.set(langui.audio ?? "Audio", []);
 | 
			
		||||
      group.set(langui.game ?? "Game", []);
 | 
			
		||||
      group.set(langui.textual ?? "Textual", []);
 | 
			
		||||
@ -334,7 +336,7 @@ function getGroups(
 | 
			
		||||
            years.push(item.attributes.release_date.year);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      const group: GroupLibraryItems = new Map();
 | 
			
		||||
      const group = new Map();
 | 
			
		||||
      years.sort((a, b) => a - b);
 | 
			
		||||
      years.map((year) => {
 | 
			
		||||
        group.set(year.toString(), []);
 | 
			
		||||
@ -352,7 +354,7 @@ function getGroups(
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default: {
 | 
			
		||||
      const group: GroupLibraryItems = new Map();
 | 
			
		||||
      const group = new Map();
 | 
			
		||||
      group.set("", items);
 | 
			
		||||
      return group;
 | 
			
		||||
    }
 | 
			
		||||
@ -363,8 +365,8 @@ function filterItems(
 | 
			
		||||
  showSubitems: boolean,
 | 
			
		||||
  showPrimaryItems: boolean,
 | 
			
		||||
  showSecondaryItems: boolean,
 | 
			
		||||
  items: Props["items"]
 | 
			
		||||
): Props["items"] {
 | 
			
		||||
  items: Immutable<Props["items"]>
 | 
			
		||||
): Immutable<Props["items"]> {
 | 
			
		||||
  return [...items].filter((item) => {
 | 
			
		||||
    if (!showSubitems && !item.attributes?.root_item) return false;
 | 
			
		||||
    if (
 | 
			
		||||
@ -384,9 +386,9 @@ function filterItems(
 | 
			
		||||
 | 
			
		||||
function sortBy(
 | 
			
		||||
  orderByType: number,
 | 
			
		||||
  items: Props["items"],
 | 
			
		||||
  items: Immutable<Props["items"]>,
 | 
			
		||||
  currencies: AppStaticProps["currencies"]
 | 
			
		||||
): Props["items"] {
 | 
			
		||||
): Immutable<Props["items"]> {
 | 
			
		||||
  switch (orderByType) {
 | 
			
		||||
    case 0:
 | 
			
		||||
      return [...items].sort((a, b) => {
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,12 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
export default function Merch(props: Props): JSX.Element {
 | 
			
		||||
export default function Merch(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,20 @@
 | 
			
		||||
import PostPage, { Post } from "components/PostPage";
 | 
			
		||||
import { GetPostQuery } from "graphql/generated";
 | 
			
		||||
import { PostPage } from "components/PostPage";
 | 
			
		||||
import { AppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import {
 | 
			
		||||
  getPostStaticProps,
 | 
			
		||||
  PostStaticProps,
 | 
			
		||||
} from "graphql/getPostStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import {
 | 
			
		||||
  GetStaticPathsContext,
 | 
			
		||||
  GetStaticPathsResult,
 | 
			
		||||
  GetStaticPropsContext,
 | 
			
		||||
} from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  post: Post;
 | 
			
		||||
  postId: Exclude<
 | 
			
		||||
    GetPostQuery["posts"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"][number]["id"];
 | 
			
		||||
}
 | 
			
		||||
interface Props extends AppStaticProps, PostStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { post, langui, languages, currencies } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <PostPage
 | 
			
		||||
@ -36,21 +34,8 @@ export default function LibrarySlug(props: Props): JSX.Element {
 | 
			
		||||
export async function getStaticProps(
 | 
			
		||||
  context: GetStaticPropsContext
 | 
			
		||||
): Promise<{ notFound: boolean } | { props: Props }> {
 | 
			
		||||
  const sdk = getReadySdk();
 | 
			
		||||
  const slug = context.params?.slug ? context.params.slug.toString() : "";
 | 
			
		||||
  const post = await sdk.getPost({
 | 
			
		||||
    slug: slug,
 | 
			
		||||
    language_code: context.locale ?? "en",
 | 
			
		||||
  });
 | 
			
		||||
  if (!post.posts?.data[0].attributes) return { notFound: true };
 | 
			
		||||
  const props: Props = {
 | 
			
		||||
    ...(await getAppStaticProps(context)),
 | 
			
		||||
    post: post.posts.data[0].attributes,
 | 
			
		||||
    postId: post.posts.data[0].id,
 | 
			
		||||
  };
 | 
			
		||||
  return {
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
  return await getPostStaticProps(slug)(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getStaticPaths(
 | 
			
		||||
 | 
			
		||||
@ -1,35 +1,30 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import Switch from "components/Inputs/Switch";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import ContentPanel, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { Switch } from "components/Inputs/Switch";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import {
 | 
			
		||||
  ContentPanel,
 | 
			
		||||
  ContentPanelWidthSizes,
 | 
			
		||||
} from "components/Panels/ContentPanel";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import ThumbnailPreview from "components/PreviewCard";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { PreviewCard } from "components/PreviewCard";
 | 
			
		||||
import { GetPostsPreviewQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettyDate, prettySlug } from "helpers/formatters";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettyDate, prettySlug } from "queries/helpers";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  posts: Exclude<GetPostsPreviewQuery["posts"], null | undefined>["data"];
 | 
			
		||||
  posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function News(props: Props): JSX.Element {
 | 
			
		||||
  const { langui, posts } = props;
 | 
			
		||||
export default function News(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const posts = sortPosts(props.posts);
 | 
			
		||||
 | 
			
		||||
  const [keepInfoVisible, setKeepInfoVisible] = useState(true);
 | 
			
		||||
 | 
			
		||||
  posts
 | 
			
		||||
    .sort((a, b) => {
 | 
			
		||||
      const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999";
 | 
			
		||||
      const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999";
 | 
			
		||||
      return dateA.localeCompare(dateB);
 | 
			
		||||
    })
 | 
			
		||||
    .reverse();
 | 
			
		||||
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
      <PanelHeader
 | 
			
		||||
@ -39,7 +34,7 @@ export default function News(props: Props): JSX.Element {
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <div className="flex flex-row gap-2 place-items-center coarse:hidden">
 | 
			
		||||
        <p className="flex-shrink-0">{"Always show info"}:</p>
 | 
			
		||||
        <p className="flex-shrink-0">{langui.always_show_info}:</p>
 | 
			
		||||
        <Switch setState={setKeepInfoVisible} state={keepInfoVisible} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </SubPanel>
 | 
			
		||||
@ -47,11 +42,14 @@ export default function News(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
  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))]">
 | 
			
		||||
      <div
 | 
			
		||||
        className="grid gap-8 items-end grid-cols-1
 | 
			
		||||
        desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"
 | 
			
		||||
      >
 | 
			
		||||
        {posts.map((post) => (
 | 
			
		||||
          <>
 | 
			
		||||
            {post.attributes && (
 | 
			
		||||
              <ThumbnailPreview
 | 
			
		||||
              <PreviewCard
 | 
			
		||||
                key={post.id}
 | 
			
		||||
                href={`/news/${post.attributes.slug}`}
 | 
			
		||||
                title={
 | 
			
		||||
@ -103,3 +101,17 @@ export async function getStaticProps(
 | 
			
		||||
    props: props,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sortPosts(
 | 
			
		||||
  posts: Immutable<Props["posts"]>
 | 
			
		||||
): Immutable<Props["posts"]> {
 | 
			
		||||
  const sortedPosts = [...posts] as Props["posts"];
 | 
			
		||||
  sortedPosts
 | 
			
		||||
    .sort((a, b) => {
 | 
			
		||||
      const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999";
 | 
			
		||||
      const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999";
 | 
			
		||||
      return dateA.localeCompare(dateB);
 | 
			
		||||
    })
 | 
			
		||||
    .reverse();
 | 
			
		||||
  return sortedPosts as Immutable<Props["posts"]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,28 +1,25 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import InsetBox from "components/InsetBox";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import ReturnButton, {
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
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/Wiki/Chronology/ChronologyYearComponent";
 | 
			
		||||
import { ContentPanel } from "components/Panels/ContentPanel";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { ChronologyYearComponent } from "components/Wiki/Chronology/ChronologyYearComponent";
 | 
			
		||||
import { useAppLayout } from "contexts/AppLayoutContext";
 | 
			
		||||
import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { getReadySdk } from "graphql/sdk";
 | 
			
		||||
import { prettySlug } from "helpers/formatters";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
import { prettySlug } from "queries/helpers";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {
 | 
			
		||||
  chronologyItems: Exclude<
 | 
			
		||||
    GetChronologyItemsQuery["chronologyItems"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  >["data"];
 | 
			
		||||
  chronologyEras: Exclude<
 | 
			
		||||
    GetErasQuery["chronologyEras"],
 | 
			
		||||
    null | undefined
 | 
			
		||||
  chronologyItems: NonNullable<
 | 
			
		||||
    GetChronologyItemsQuery["chronologyItems"]
 | 
			
		||||
  >["data"];
 | 
			
		||||
  chronologyEras: NonNullable<GetErasQuery["chronologyEras"]>["data"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function Chronology(props: Props): JSX.Element {
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,14 @@
 | 
			
		||||
import AppLayout from "components/AppLayout";
 | 
			
		||||
import NavOption from "components/PanelComponents/NavOption";
 | 
			
		||||
import PanelHeader from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import SubPanel from "components/Panels/SubPanel";
 | 
			
		||||
import { AppLayout } from "components/AppLayout";
 | 
			
		||||
import { NavOption } from "components/PanelComponents/NavOption";
 | 
			
		||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
 | 
			
		||||
import { SubPanel } from "components/Panels/SubPanel";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
 | 
			
		||||
import { Immutable } from "helpers/types";
 | 
			
		||||
import { GetStaticPropsContext } from "next";
 | 
			
		||||
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
 | 
			
		||||
 | 
			
		||||
interface Props extends AppStaticProps {}
 | 
			
		||||
 | 
			
		||||
export default function Wiki(props: Props): JSX.Element {
 | 
			
		||||
export default function Wiki(props: Immutable<Props>): JSX.Element {
 | 
			
		||||
  const { langui } = props;
 | 
			
		||||
  const subPanel = (
 | 
			
		||||
    <SubPanel>
 | 
			
		||||
 | 
			
		||||
@ -159,5 +159,13 @@ module.exports = {
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    plugin(function ({ addUtilities }) {
 | 
			
		||||
      addUtilities({
 | 
			
		||||
        ".break-words": {
 | 
			
		||||
          "word-break": "break-word",
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user