Updating to Next.js 13 #64

Closed
DrMint wants to merge 6 commits from next13 into main
55 changed files with 703 additions and 10663 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ src/graphql/generated.ts
# next.js
/.next/
/out/
src/pages.old
# production
/build

View File

@ -1 +1,2 @@
.next
public/local-data

View File

@ -1,17 +1,9 @@
/* CONFIG */
const locales = ["en", "es", "fr", "pt-br", "ja"];
/* END CONFIG */
/* @type {import('next').NextConfig} */
module.exports = {
swcMinify: true,
reactStrictMode: true,
poweredByHeader: false,
i18n: {
locales: locales,
defaultLocale: "en",
experimental: {
appDir: true,
},
images: {
domains: ["img.accords-library.com", "watch.accords-library.com"],

1395
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,60 +17,60 @@
"prettier": "prettier --end-of-line auto --write ."
},
"dependencies": {
"@fontsource/material-icons": "^4.5.4",
"@fontsource/material-icons-outlined": "^4.5.4",
"@fontsource/opendyslexic": "^4.5.4",
"@fontsource/share-tech-mono": "^4.5.9",
"@fontsource/vollkorn": "^4.5.12",
"@fontsource/zen-maru-gothic": "^4.5.13",
"@tippyjs/react": "^4.2.6",
"autoprefixer": "^10.4.13",
"cuid": "^2.1.8",
"graphql-request": "^5.0.0",
"jotai": "^1.9.0",
"markdown-to-jsx": "^7.1.7",
"next": "^12.3.1",
"nodemailer": "^6.8.0",
"rc-slider": "^10.0.1",
"@fontsource/material-icons": "4.5.4",
"@fontsource/material-icons-outlined": "4.5.4",
"@fontsource/opendyslexic": "4.5.4",
"@fontsource/share-tech-mono": "4.5.9",
"@fontsource/vollkorn": "4.5.12",
"@fontsource/zen-maru-gothic": "4.5.13",
"@tippyjs/react": "4.2.6",
"autoprefixer": "10.4.13",
"graphql-request": "5.0.0",
"jotai": "1.9.1",
"markdown-to-jsx": "7.1.7",
"next": "13.0.2",
"nodemailer": "6.8.0",
"rc-slider": "10.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hotkeys-hook": "^3.4.7",
"react-swipeable": "^7.0.0",
"react-zoom-pan-pinch": "^2.1.3",
"string-natural-compare": "^3.0.1",
"throttle-debounce": "^5.0.0",
"tippy.js": "^6.3.7",
"turndown": "^7.1.1",
"ua-parser-js": "^1.0.32",
"usehooks-ts": "^2.9.1"
"react-hotkeys-hook": "3.4.7",
"react-swipeable": "7.0.0",
"react-zoom-pan-pinch": "2.1.3",
"string-natural-compare": "3.0.1",
"throttle-debounce": "5.0.0",
"tippy.js": "6.3.7",
"turndown": "7.1.1",
"ua-parser-js": "1.0.32",
"usehooks-ts": "2.9.1"
},
"devDependencies": {
"@digitak/esrun": "^3.2.14",
"@graphql-codegen/cli": "^2.13.8",
"@graphql-codegen/typescript": "2.8.0",
"@graphql-codegen/typescript-graphql-request": "^4.5.7",
"@graphql-codegen/typescript-operations": "^2.5.5",
"@types/node": "18.11.7",
"@types/nodemailer": "^6.4.6",
"@digitak/esrun": "3.2.14",
"@graphql-codegen/cli": "2.13.11",
"@graphql-codegen/typescript": "2.8.1",
"@graphql-codegen/typescript-graphql-request": "4.5.8",
"@graphql-codegen/typescript-operations": "2.5.6",
"@types/node": "18.11.9",
"@types/nodemailer": "6.4.6",
"@types/react": "18.0.22",
"@types/react-dom": "^18.0.8",
"@types/string-natural-compare": "^3.0.2",
"@types/throttle-debounce": "^5.0.0",
"@types/turndown": "^5.0.1",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^5.41.0",
"@typescript-eslint/parser": "^5.41.0",
"dotenv": "^16.0.3",
"eslint": "^8.26.0",
"eslint-config-next": "12.3.1",
"eslint-plugin-import": "^2.26.0",
"graphql": "^16.6.0",
"next-sitemap": "^3.1.29",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
"tailwindcss": "^3.2.1",
"ts-unused-exports": "^8.0.0",
"typescript": "^4.8.4"
"@types/react-dom": "18.0.8",
"@types/string-natural-compare": "3.0.2",
"@types/throttle-debounce": "5.0.0",
"@types/turndown": "5.0.1",
"@types/ua-parser-js": "0.7.36",
"@typescript-eslint/eslint-plugin": "5.42.0",
"@typescript-eslint/parser": "5.42.0",
"cuid": "2.1.8",
"dotenv": "16.0.3",
"eslint": "8.26.0",
"eslint-config-next": "13.0.2",
"eslint-plugin-import": "2.26.0",
"graphql": "16.6.0",
"next-sitemap": "3.1.30",
"prettier": "2.7.1",
"prettier-plugin-tailwindcss": "0.1.13",
"tailwindcss": "3.2.1",
"ts-unused-exports": "8.0.0",
"typescript": "4.8.4"
},
"overrides": {
"react-zoom-pan-pinch": {

View File

@ -0,0 +1,2 @@
const Page: Page = () => <>Hello from contents</>;
export default Page;

View File

@ -0,0 +1,8 @@
export const generateStaticParams: GenerateStaticParams = () =>
["en", "es", "fr", "pt-br", "ja", ""].map((locale) => ({ locale }));
// Disabled using locales other than the one defined
export const dynamicParams = false;
const Page: Page = () => <>Hello from within locale</>;
export default Page;

19
src/app/head.tsx Normal file
View File

@ -0,0 +1,19 @@
const Head: Head = () => (
<>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#9c6644" />
<meta name="apple-mobile-web-app-title" content="Accord's Library" />
<meta name="application-name" content="Accord's Library" />
<meta name="msapplication-TileColor" content="#feecd6" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#feecd6" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#26221e" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
</>
);
export default Head;

64
src/app/layout.tsx Normal file
View File

@ -0,0 +1,64 @@
"use client";
import "@fontsource/material-icons";
import "@fontsource/material-icons-outlined";
import "@fontsource/opendyslexic/400.css";
import "@fontsource/share-tech-mono/400.css";
import "@fontsource/opendyslexic/700.css";
import "@fontsource/vollkorn/700.css";
import "@fontsource/zen-maru-gothic/500.css";
import "@fontsource/zen-maru-gothic/900.css";
import "styles/debug.css";
import "styles/formatted.css";
import "styles/others.css";
import "styles/rc-slider.css";
import "styles/tippy.css";
import Script from "next/script";
import { useLocalData } from "contexts/localData";
// import { useAppLayout } from "contexts/appLayout";
import { LightBoxProvider } from "contexts/LightBoxProvider";
import { SettingsPopup } from "components/Panels/SettingsPopup";
import { useSettings } from "contexts/settings";
import { useContainerQueries } from "contexts/containerQueries";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { cIf, cJoin } from "helpers/className";
const Layout: Layout = ({ children }) => {
useLocalData();
// useAppLayout();
useSettings();
useContainerQueries();
const isDyslexic = useAtomGetter(atoms.settings.dyslexic);
const isDarkMode = useAtomGetter(atoms.settings.darkMode);
return (
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body
id={Ids.Body}
className={cJoin(
"bg-light font-body font-medium text-black",
cIf(isDyslexic, "set-theme-font-dyslexic", "set-theme-font-standard"),
cIf(isDarkMode, "set-theme-dark", "set-theme-light")
)}>
<SettingsPopup />
<LightBoxProvider />
<Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
/>
{children}
</body>
</html>
);
};
export default Layout;

View File

@ -234,7 +234,7 @@ export const Terminal = ({
}, [line]);
return (
<div className={cJoin("h-screen overflow-hidden bg-light set-theme-font-standard")}>
<div className="h-screen overflow-hidden bg-light set-theme-font-standard">
<div
ref={terminalWindowRef}
className="h-full overflow-scroll scroll-auto p-6 scrollbar-none">

View File

@ -32,8 +32,6 @@ export const SettingsPopup = (): JSX.Element => {
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const router = useRouter();
const currencyOptions = useMemo(
() =>
filterHasAttributes(currencies, ["attributes"] as const).map(
@ -66,34 +64,6 @@ export const SettingsPopup = (): JSX.Element => {
`mt-4 grid justify-items-center gap-16 text-center`,
cIf(!is1ColumnLayout, "grid-cols-[auto_auto]")
)}>
{router.locales && (
<div>
<h3 className="text-xl">{langui.languages}</h3>
{preferredLanguages.length > 0 && (
<OrderableList
items={preferredLanguages.map((locale) => ({
code: locale,
name: prettyLanguage(locale, languages),
}))}
insertLabels={[
{
insertAt: 0,
name: langui.primary_language ?? "Primary language",
},
{
insertAt: 1,
name: langui.secondary_language ?? "Secondary languages",
},
]}
onChange={(items) => {
const newPreferredLanguages = items.map((item) => item.code);
setPreferredLanguages(newPreferredLanguages);
sendAnalytics("Settings", "Change preferred languages");
}}
/>
)}
</div>
)}
<div
className={cJoin(
"grid place-items-center gap-8 text-center",

View File

@ -1,28 +1,17 @@
import React, { useCallback, useState } from "react";
import { useEffectOnce } from "usehooks-ts";
import { atom } from "jotai";
import { UploadImageFragment } from "graphql/generated";
import { LightBox } from "components/LightBox";
import { filterDefined } from "helpers/others";
import { atomPairing, useAtomSetter } from "helpers/atoms";
const lightBoxAtom = atomPairing(
atom<{
showLightBox: (
images: (UploadImageFragment | string | null | undefined)[],
index?: number
) => void;
}>({ showLightBox: () => null })
);
export const lightBox = lightBoxAtom[0];
import { useAtomSetter } from "helpers/atoms";
import { lightBox } from "contexts/atoms";
export const LightBoxProvider = (): JSX.Element => {
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
const [lightBoxIndex, setLightBoxIndex] = useState(0);
const setShowLightBox = useAtomSetter(lightBoxAtom);
const setShowLightBox = useAtomSetter(lightBox);
useEffectOnce(() =>
setShowLightBox({

View File

@ -1,8 +1,8 @@
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useScrollIntoView } from "hooks/useScrollIntoView";
import { useAtomSetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
import { useScrollIntoView } from "hooks/useScrollIntoView";
export const useAppLayout = (): void => {
const router = useRouter();

View File

@ -4,13 +4,24 @@ import { localData } from "contexts/localData";
import { containerQueries } from "contexts/containerQueries";
import { atomPairing } from "helpers/atoms";
import { settings } from "contexts/settings";
import { lightBox } from "contexts/LightBoxProvider";
import { UploadImageFragment } from "graphql/generated";
/*
* I'm getting a weird error if I put those atoms in appLayout.ts
* So I'm putting the atoms here. Sucks, I know.
*/
/* [ LIGHTBOX ] */
export const lightBox = atomPairing(
atom<{
showLightBox: (
images: (UploadImageFragment | string | null | undefined)[],
index?: number
) => void;
}>({ showLightBox: () => null })
);
/* [ APPLAYOUT ATOMS ] */
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
@ -44,6 +55,6 @@ export const atoms = {
layout,
terminal,
localData,
lightBox,
lightBox: lightBox[0],
containerQueries,
};

View File

@ -35,7 +35,6 @@ export const useLocalData = (): void => {
const setCurrencies = useAtomSetter(currencies);
const setLangui = useAtomSetter(langui);
const { locale } = useRouter();
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
const { data: rawCurrencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
@ -54,6 +53,6 @@ export const useLocalData = (): void => {
useEffect(() => {
console.log("[useLocalData] Refresh langui");
setLangui(processLangui(rawLangui, locale));
}, [locale, rawLangui, setLangui]);
setLangui(processLangui(rawLangui, "en"));
}, [rawLangui, setLangui]);
};

View File

@ -1,11 +1,12 @@
import { useRouter } from "next/router";
// import { useRouter } from "next/router";
import { useLayoutEffect, useEffect } from "react";
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
import { atomPairing, useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
import { getDefaultPreferredLanguages } from "helpers/locales";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { usePrefersDarkMode } from "hooks/useMediaQuery";
import { Ids } from "types/ids";
export enum ThemeMode {
Dark = "dark",
@ -32,11 +33,10 @@ export const settings = {
};
export const useSettings = (): void => {
const router = useRouter();
const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
// const router = useRouter();
// const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
const fontSize = useAtomGetter(fontSizeAtom);
const isDyslexic = useAtomGetter(dyslexicAtom);
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
const setDarkMode = useAtomSetter(darkModeAtom);
const themeMode = useAtomGetter(themeModeAtom);
useLayoutEffect(() => {
@ -46,19 +46,6 @@ export const useSettings = (): void => {
}
}, [fontSize]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (isDyslexic) {
next.classList.add("set-theme-font-dyslexic");
next.classList.remove("set-theme-font-standard");
} else {
next.classList.add("set-theme-font-standard");
next.classList.remove("set-theme-font-dyslexic");
}
}
}, [isDyslexic]);
/* DARK MODE */
const prefersDarkMode = usePrefersDarkMode();
@ -66,31 +53,21 @@ export const useSettings = (): void => {
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
}, [prefersDarkMode, setDarkMode, themeMode]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (isDarkMode) {
next.classList.add("set-theme-dark");
next.classList.remove("set-theme-light");
} else {
next.classList.add("set-theme-light");
next.classList.remove("set-theme-dark");
}
}
}, [isDarkMode]);
/* PREFERRED LANGUAGES */
/*
useEffect(() => {
if (preferredLanguages.length === 0) {
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
}
} else if (router.locale !== preferredLanguages[0]) {
/*
* Using a timeout to the code getting stuck into a loop when reaching the website with a
* different preferredLanguages[0] from router.locale
*/
*/
/*
* Using a timeout to the code getting stuck into a loop when reaching the website with a
* different preferredLanguages[0] from router.locale
*/
/*
setTimeout(
async () =>
router.replace(router.asPath, router.asPath, {
@ -100,4 +77,5 @@ export const useSettings = (): void => {
);
}
}, [preferredLanguages, router, setPreferredLanguages]);
*/
};

View File

@ -1,54 +0,0 @@
import { GetStaticProps } from "next";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const FourOhFour = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
contentPanel={
<ContentPanel>
<Img
src={"/gameover_cards.webp"}
className="animate-zoom-in drop-shadow-lg shadow-shade"
/>
<div className="mt-8 grid place-items-center gap-6">
<h2>{langui.page_not_found}</h2>
<ReturnButton href="/" title="Home" />
</div>
</ContentPanel>
}
openGraph={openGraph}
{...otherProps}
/>
);
};
export default FourOhFour;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, `404 - ${langui.page_not_found}`),
};
return {
props: props,
};
};

View File

@ -1,54 +0,0 @@
import { GetStaticProps } from "next";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const FiveHundred = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
contentPanel={
<ContentPanel>
<Img
src={"/gameover_cards.webp"}
className="animate-zoom-in drop-shadow-lg shadow-shade"
/>
<div className="mt-8 grid place-items-center gap-6">
<h2>{langui.page_not_found}</h2>
<ReturnButton href="/" title="Home" />
</div>
</ContentPanel>
}
openGraph={openGraph}
{...otherProps}
/>
);
};
export default FiveHundred;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "500 - Internal Server Error"),
};
return {
props: props,
};
};

View File

@ -1,46 +0,0 @@
import "@fontsource/material-icons";
import "@fontsource/material-icons-outlined";
import "@fontsource/opendyslexic/400.css";
import "@fontsource/share-tech-mono/400.css";
import "@fontsource/opendyslexic/700.css";
import "@fontsource/vollkorn/700.css";
import "@fontsource/zen-maru-gothic/500.css";
import "@fontsource/zen-maru-gothic/900.css";
import type { AppProps } from "next/app";
import Script from "next/script";
import "styles/debug.css";
import "styles/formatted.css";
import "styles/others.css";
import "styles/rc-slider.css";
import "styles/tippy.css";
import { useLocalData } from "contexts/localData";
import { useAppLayout } from "contexts/appLayout";
import { LightBoxProvider } from "contexts/LightBoxProvider";
import { SettingsPopup } from "components/Panels/SettingsPopup";
import { useSettings } from "contexts/settings";
import { useContainerQueries } from "contexts/containerQueries";
const AccordsLibraryApp = (props: AppProps): JSX.Element => {
useLocalData();
useAppLayout();
useSettings();
useContainerQueries();
return (
<>
<SettingsPopup />
<LightBoxProvider />
<Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
/>
<props.Component {...props.pageProps} />
</>
);
};
export default AccordsLibraryApp;

View File

@ -1,42 +0,0 @@
import Document, {
DocumentContext,
DocumentInitialProps,
Head,
Html,
Main,
NextScript,
} from "next/document";
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext): Promise<DocumentInitialProps> {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render(): JSX.Element {
return (
<Html>
<Head>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#9c6644" />
<meta name="apple-mobile-web-app-title" content="Accord's Library" />
<meta name="application-name" content="Accord's Library" />
<meta name="msapplication-TileColor" content="#feecd6" />
<meta name="msapplication-TileImage" content="/mstile-144x144.png" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#feecd6" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#26221e" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

View File

@ -1,30 +0,0 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
const AccordsHandbook = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/>
);
};
export default AccordsHandbook;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("accords-handbook");

View File

@ -1,185 +0,0 @@
import { useRouter } from "next/router";
import { useState } from "react";
import { InsetBox } from "components/Containers/InsetBox";
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { cIf, cJoin } from "helpers/className";
import { randomInt } from "helpers/numbers";
import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
const AboutUs = (props: PostStaticProps): JSX.Element => {
const router = useRouter();
const langui = useAtomGetter(atoms.localData.langui);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const [formResponse, setFormResponse] = useState("");
const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">("stale");
const [randomNumber1, setRandomNumber1] = useState(randomInt(0, 10));
const [randomNumber2, setRandomNumber2] = useState(randomInt(0, 10));
const contactForm = (
<div className="flex flex-col gap-8 text-center">
<form
className={cJoin(
"grid gap-8",
cIf(formState !== "stale", "pointer-events-none cursor-not-allowed touch-none opacity-60")
)}
onSubmit={(event) => {
event.preventDefault();
const fields = event.target as unknown as {
verif: HTMLInputElement;
name: HTMLInputElement;
email: HTMLInputElement;
message: HTMLInputElement;
};
setFormState("ongoing");
if (
parseInt(fields.verif.value, 10) === randomNumber1 + randomNumber2 &&
formState !== "completed"
) {
const content: RequestMailProps = {
name: fields.name.value,
email: fields.email.value,
message: fields.message.value,
formName: "Contact Form",
};
fetch("/api/mail", {
method: "POST",
body: JSON.stringify(content),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then(async (responseJson) => responseJson.json())
.then((response: ResponseMailProps) => {
switch (response.code) {
case "OKAY":
setFormResponse(langui.response_email_success ?? "");
setFormState("completed");
sendAnalytics("Contact", "Send email (success)");
break;
case "EENVELOPE":
setFormResponse(langui.response_invalid_email ?? "");
setFormState("stale");
sendAnalytics("Contact", "Send email (invalid email)");
break;
default:
setFormResponse(response.message ?? "");
setFormState("stale");
sendAnalytics("Contact", "Send email (error)");
break;
}
});
} else {
setFormResponse(langui.response_invalid_code ?? "");
setFormState("stale");
setRandomNumber1(randomInt(0, 10));
setRandomNumber2(randomInt(0, 10));
}
router.replace("#send-response");
fields.verif.value = "";
}}>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="name">{langui.name}:</label>
<input
type="text"
className={cIf(is1ColumnLayout, "w-full")}
name="name"
id="name"
required
disabled={formState !== "stale"}
/>
</div>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="email">{langui.email}:</label>
<input
type="email"
className={cIf(is1ColumnLayout, "w-full")}
name="email"
id="email"
required
disabled={formState !== "stale"}
/>
<p className="text-sm italic text-dark opacity-70">{langui.email_gdpr_notice}</p>
</div>
<div className="flex w-full flex-col place-items-center gap-1">
<label htmlFor="message">{langui.message}:</label>
<textarea
name="message"
id="message"
className="w-full"
rows={8}
required
disabled={formState !== "stale"}
/>
</div>
<div className="grid grid-cols-2 place-items-center">
<div className="flex flex-row place-items-center gap-2">
<label
className="flex-shrink-0"
htmlFor="verif">{`${randomNumber1} + ${randomNumber2} =`}</label>
<input
className="w-24"
type="number"
name="verif"
id="verif"
required
disabled={formState !== "stale"}
/>
</div>
<input
type="submit"
value={langui.send ?? "Send"}
className="w-min !px-6"
disabled={formState !== "stale"}
/>
</div>
</form>
<div id="send-response">
{formResponse && (
<InsetBox>
<p>{formResponse}</p>
</InsetBox>
)}
</div>
</div>
);
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
appendBody={contactForm}
displayLanguageSwitcher
/>
);
};
export default AboutUs;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("contact");

View File

@ -1,59 +0,0 @@
import { GetStaticProps } from "next";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Icon } from "components/Ico";
import { NavOption } from "components/PanelComponents/NavOption";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const AboutUs = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
subPanel={
<SubPanel>
<PanelHeader
icon={Icon.Info}
title={langui.about_us}
description={langui.about_us_description}
/>
<HorizontalLine />
<NavOption title={langui.accords_handbook} url="/about-us/accords-handbook" border />
<NavOption title={langui.legality} url="/about-us/legality" border />
<NavOption title={langui.sharing_policy} url="/about-us/sharing-policy" border />
<NavOption title={langui.contact_us} url="/about-us/contact" border />
</SubPanel>
}
{...props}
/>
);
};
export default AboutUs;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.about_us ?? "About us"),
};
return {
props: props,
};
};

View File

@ -1,30 +0,0 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
const Legality = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/>
);
};
export default Legality;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("legality");

View File

@ -1,30 +0,0 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
const SharingPolicy = (props: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/>
);
};
export default SharingPolicy;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("sharing-policy");

View File

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

View File

@ -1,253 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { i18n } from "../../../next.config";
import { cartesianProduct } from "helpers/others";
type CRUDEvents = "entry.create" | "entry.delete" | "entry.update";
type StrapiEvent = {
event: CRUDEvents;
model: string;
entry: Record<string, unknown>;
};
type RequestProps =
| CustomRequest
| StrapiChronicle
| StrapiChronicleChapter
| StrapiChronology
| StrapiContent
| StrapiContentFolder
| StrapiLibraryItem
| StrapiPostContent
| StrapiRangedContent
| StrapiWiki;
interface CustomRequest {
model: "custom";
path: string;
}
interface StrapiRangedContent extends StrapiEvent {
event: CRUDEvents;
model: "ranged-content";
entry: {
library_item?: {
slug: string;
};
content?: {
slug: string;
};
};
}
interface StrapiContent extends StrapiEvent {
model: "content";
entry: {
slug: string;
folder?: {
slug: string;
};
ranged_contents: {
slug: string;
}[];
};
}
interface StrapiPostContent extends StrapiEvent {
event: CRUDEvents;
model: "post";
entry: {
slug: string;
};
}
interface StrapiLibraryItem extends StrapiEvent {
event: CRUDEvents;
model: "library-item";
entry: {
slug: string;
subitem_of: [
{
slug: string;
}
];
};
}
interface StrapiContentFolder extends StrapiEvent {
event: CRUDEvents;
model: "contents-folder";
entry: {
slug: string;
parent_folder?: {
slug: string;
};
subfolders: { slug: string }[];
contents: {
slug: string;
}[];
};
}
interface StrapiChronology extends StrapiEvent {
event: CRUDEvents;
model: "chronology-era" | "chronology-item";
}
interface StrapiWiki extends StrapiEvent {
event: CRUDEvents;
model: "wiki-page";
entry: {
slug: string;
};
}
interface StrapiChronicle extends StrapiEvent {
event: CRUDEvents;
model: "chronicle";
entry: {
slug: string;
};
}
interface StrapiChronicleChapter extends StrapiEvent {
event: CRUDEvents;
model: "chronicles-chapter";
entry: {
chronicles: { slug: string }[];
};
}
type ResponseMailProps = {
message: string;
revalidated: boolean;
};
const Revalidate = (req: NextApiRequest, res: NextApiResponse<ResponseMailProps>): void => {
const body = req.body as RequestProps;
// Check for secret to confirm this is a valid request
if (req.headers.authorization !== `Bearer ${process.env.REVALIDATION_TOKEN}`) {
res.status(401).json({ message: "Invalid token", revalidated: false });
return;
}
const paths: string[] = [];
switch (body.model) {
case "post": {
paths.push(`/news`);
paths.push(`/news/${body.entry.slug}`);
break;
}
case "library-item": {
paths.push(`/library`);
paths.push(`/library/${body.entry.slug}`);
paths.push(`/library/${body.entry.slug}/reader`);
body.entry.subitem_of.forEach((parentItem) => {
paths.push(`/library/${parentItem.slug}`);
});
break;
}
case "content": {
paths.push(`/contents`);
paths.push(`/contents/all`);
paths.push(`/contents/${body.entry.slug}`);
if (body.entry.folder?.slug) {
paths.push(`/contents/folder/${body.entry.folder.slug}`);
}
if (body.entry.ranged_contents.length > 0) {
body.entry.ranged_contents.forEach((ranged_content) => {
const parentSlug = ranged_content.slug.slice(
0,
ranged_content.slug.length - body.entry.slug.length - 1
);
paths.push(`/library/${parentSlug}`);
paths.push(`/library/${parentSlug}/reader`);
});
}
break;
}
case "chronology-era":
case "chronology-item": {
paths.push(`/wiki/chronology`);
break;
}
case "ranged-content": {
if (body.entry.library_item) {
paths.push(`/library/${body.entry.library_item.slug}`);
paths.push(`/library/${body.entry.library_item.slug}/reader`);
}
if (body.entry.content) {
paths.push(`/contents/${body.entry.content.slug}`);
}
break;
}
case "contents-folder": {
if (body.entry.slug === "root") {
paths.push(`/contents`);
}
paths.push(`/contents/folder/${body.entry.slug}`);
if (body.entry.parent_folder) {
paths.push(`/contents/folder/${body.entry.parent_folder.slug}`);
}
body.entry.subfolders.forEach((subfolder) =>
paths.push(`/contents/folder/${subfolder.slug}`)
);
body.entry.contents.forEach((content) => paths.push(`/contents/${content.slug}`));
break;
}
case "wiki-page": {
paths.push(`/wiki`);
paths.push(`/wiki/${body.entry.slug}`);
break;
}
case "chronicle": {
paths.push(`/chronicles`);
paths.push(`/chronicles/${body.entry.slug}`);
break;
}
case "chronicles-chapter": {
paths.push(`/chronicles`);
body.entry.chronicles.forEach((chronicle) => {
paths.push(`/chronicles/${chronicle.slug}`);
});
break;
}
case "custom": {
paths.push(`${body.path}`);
break;
}
default:
console.log(body);
break;
}
const localizedPaths = cartesianProduct(i18n.locales, paths).map(
([locale, path]) => `/${locale}${path}`
);
console.table(localizedPaths);
try {
Promise.all(
localizedPaths.map(async (path) => {
await res.revalidate(path);
})
);
res.json({ message: "Success!", revalidated: true });
return;
} catch (error) {
res.status(500).send({ message: `Error revalidating: ${error}`, revalidated: false });
}
};
export default Revalidate;

View File

@ -1,54 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { NavOption } from "components/PanelComponents/NavOption";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const Archives = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Inventory}
title={langui.archives}
description={langui.archives_description}
/>
<HorizontalLine />
<NavOption title={"Videos"} url="/archives/videos/" border />
</SubPanel>
),
[langui]
);
return <AppLayout subPanel={subPanel} {...props} />;
};
export default Archives;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.archives ?? "Archives"),
};
return {
props: props,
};
};

View File

@ -1,180 +0,0 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo, useState } from "react";
import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import { GetVideoChannelQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { getVideoThumbnailURL } from "helpers/videos";
import { Icon } from "components/Ico";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel";
import { filterHasAttributes, isDefined } from "helpers/others";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
import { SmartList } from "components/SmartList";
import { cIf } from "helpers/className";
import { TextInput } from "components/Inputs/TextInput";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
searchName: "",
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
channel: NonNullable<GetVideoChannelQuery["videoChannels"]>["data"][number]["attributes"];
}
const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(true);
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
href="/archives/videos/"
title={langui.videos}
displayOnlyOn={"3ColumnsLayout"}
className="mb-10"
/>
<PanelHeader
icon={Icon.Movie}
title={langui.videos}
description={langui.archives_description}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search title..."}
value={searchName}
onChange={setSearchName}
/>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel>
)}
</SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(channel?.videos?.data, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<PreviewCard
href={`/archives/videos/v/${item.attributes.uid}`}
title={item.attributes.title}
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnailAspectRatio="16/9"
thumbnailForceAspectRatio
keepInfoVisible={keepInfoVisible}
metadata={{
releaseDate: item.attributes.published_date,
views: item.attributes.views,
author: channel?.title,
position: "Top",
}}
hoverlay={{
__typename: "Video",
duration: item.attributes.duration,
}}
/>
)}
className={cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5"
)}
groupingFunction={() => [channel?.title ?? ""]}
paginationItemPerPage={25}
searchingTerm={searchName}
searchingBy={(item) => item.attributes.title}
/>
</ContentPanel>
),
[channel?.title, channel?.videos?.data, isContentPanelAtLeast4xl, keepInfoVisible, searchName]
);
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
};
export default Channel;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const channel = await sdk.getVideoChannel({
channel: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
});
if (!channel.videoChannels?.data[0].attributes) return { notFound: true };
channel.videoChannels.data[0].attributes.videos?.data
.sort((a, b) => compareDate(a.attributes?.published_date, b.attributes?.published_date))
.reverse();
const props: Props = {
channel: channel.videoChannels.data[0].attributes,
openGraph: getOpenGraph(langui, channel.videoChannels.data[0].attributes.title),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const channels = await sdk.getVideoChannelsSlugs();
const paths: GetStaticPathsResult["paths"] = [];
if (channels.videoChannels?.data)
filterHasAttributes(channels.videoChannels.data, ["attributes"] as const).map((channel) => {
context.locales?.map((local) => {
paths.push({
params: { uid: channel.attributes.uid },
locale: local,
});
});
});
return {
paths,
fallback: "blocking",
};
};

View File

@ -1,150 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo, useState } from "react";
import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { SmartList } from "components/SmartList";
import { Icon } from "components/Ico";
import { Switch } from "components/Inputs/Switch";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import { GetVideosPreviewQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes } from "helpers/others";
import { getVideoThumbnailURL } from "helpers/videos";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
searchName: "",
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
videos: NonNullable<GetVideosPreviewQuery["videos"]>["data"];
}
const Videos = ({ videos, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(true);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
href="/archives/"
title={"Archives"}
displayOnlyOn={"3ColumnsLayout"}
className="mb-10"
/>
<PanelHeader icon={Icon.Movie} title="Videos" description={langui.archives_description} />
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search title..."}
value={searchName}
onChange={setSearchName}
/>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
</WithLabel>
)}
</SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(videos, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<PreviewCard
href={`/archives/videos/v/${item.attributes.uid}`}
title={item.attributes.title}
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnailAspectRatio="16/9"
thumbnailForceAspectRatio
keepInfoVisible={keepInfoVisible}
metadata={{
releaseDate: item.attributes.published_date,
views: item.attributes.views,
author: item.attributes.channel?.data?.attributes?.title,
position: "Top",
}}
hoverlay={{
__typename: "Video",
duration: item.attributes.duration,
}}
/>
)}
className={cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5"
)}
paginationItemPerPage={25}
searchingTerm={searchName}
searchingBy={(item) => item.attributes.title}
/>
</ContentPanel>
),
[isContentPanelAtLeast4xl, keepInfoVisible, searchName, videos]
);
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
};
export default Videos;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const videos = await sdk.getVideosPreview();
if (!videos.videos) return { notFound: true };
videos.videos.data
.sort((a, b) => compareDate(a.attributes?.published_date, b.attributes?.published_date))
.reverse();
const props: Props = {
videos: videos.videos.data,
openGraph: getOpenGraph(langui, langui.videos ?? "Videos"),
};
return {
props: props,
};
};

View File

@ -1,195 +0,0 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo } from "react";
import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { HorizontalLine } from "components/HorizontalLine";
import { Ico, Icon } from "components/Ico";
import { Button } from "components/Inputs/Button";
import { InsetBox } from "components/Containers/InsetBox";
import { NavOption } from "components/PanelComponents/NavOption";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { GetVideoQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/others";
import { getVideoFile } from "helpers/videos";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
video: NonNullable<NonNullable<GetVideoQuery["videos"]>["data"][number]["attributes"]>;
}
const Video = ({ video, ...otherProps }: Props): JSX.Element => {
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const langui = useAtomGetter(atoms.localData.langui);
const router = useRouter();
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
href="/archives/videos/"
title={langui.videos}
displayOnlyOn={"3ColumnsLayout"}
/>
<HorizontalLine />
<NavOption title={langui.video} url="#video" border />
<NavOption title={langui.channel} url="#channel" border />
<NavOption title={langui.description} url="#description" border />
</SubPanel>
),
[langui]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<ReturnButton
href="/library/"
title={langui.library}
displayOnlyOn={"1ColumnLayout"}
className="mb-10"
/>
<div className="grid place-items-center gap-12">
<div id="video" className="w-full overflow-hidden rounded-xl shadow-xl shadow-shade/80">
{video.gone ? (
<video className="w-full" src={getVideoFile(video.uid)} controls />
) : (
<iframe
src={`https://www.youtube-nocookie.com/embed/${video.uid}`}
className="aspect-video w-full"
title="YouTube video player"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write;
encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
)}
<div className="mt-2 p-6">
<h1 className="text-2xl">{video.title}</h1>
<div className="flex w-full flex-row flex-wrap gap-x-6">
<p>
<Ico icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" />
{prettyDate(video.published_date, router.locale)}
</p>
<p>
<Ico icon={Icon.Visibility} className="mr-1 translate-y-[.15em] !text-base" />
{isContentPanelAtLeast4xl
? video.views.toLocaleString()
: prettyShortenNumber(video.views)}
</p>
{video.channel?.data?.attributes && (
<p>
<Ico icon={Icon.ThumbUp} className="mr-1 translate-y-[.15em] !text-base" />
{isContentPanelAtLeast4xl
? video.likes.toLocaleString()
: prettyShortenNumber(video.likes)}
</p>
)}
<a href={`https://youtu.be/${video.uid}`} target="_blank" rel="noreferrer">
<Button className="!py-0 !px-3" text={`${langui.view_on} ${video.source}`} />
</a>
</div>
</div>
</div>
{video.channel?.data?.attributes && (
<InsetBox id="channel" className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-4 text-center">
<h2 className="text-2xl">{langui.channel}</h2>
<div>
<Button
href={`/archives/videos/c/${video.channel.data.attributes.uid}`}
text={video.channel.data.attributes.title}
/>
<p>
{`${video.channel.data.attributes.subscribers.toLocaleString()}
${langui.subscribers?.toLowerCase()}`}
</p>
</div>
</div>
</InsetBox>
)}
<InsetBox id="description" className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
<h2 className="text-2xl">{langui.description}</h2>
<p className="whitespace-pre-line">{video.description}</p>
</div>
</InsetBox>
</div>
</ContentPanel>
),
[
isContentPanelAtLeast4xl,
langui,
router.locale,
video.channel?.data?.attributes,
video.description,
video.gone,
video.likes,
video.published_date,
video.source,
video.title,
video.uid,
video.views,
]
);
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
};
export default Video;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const videos = await sdk.getVideo({
uid: context.params && isDefined(context.params.uid) ? context.params.uid.toString() : "",
});
if (!videos.videos?.data[0]?.attributes) return { notFound: true };
const props: Props = {
video: videos.videos.data[0].attributes,
openGraph: getOpenGraph(langui, videos.videos.data[0].attributes.title),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const videos = await sdk.getVideosSlugs();
const paths: GetStaticPathsResult["paths"] = [];
if (videos.videos?.data)
filterHasAttributes(videos.videos.data, ["attributes"] as const).map((video) => {
context.locales?.map((local) => {
paths.push({ params: { uid: video.attributes.uid }, locale: local });
});
});
return {
paths,
fallback: "blocking",
};
};

View File

@ -1,285 +0,0 @@
import { GetStaticProps, GetStaticPaths, GetStaticPathsResult } from "next";
import { useCallback, useMemo } from "react";
import { getReadySdk } from "graphql/sdk";
import { isDefined, filterHasAttributes } from "helpers/others";
import { ChronicleWithTranslations } from "types/types";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { ContentPanel } from "components/Containers/ContentPanel";
import { Markdawn } from "components/Markdown/Markdawn";
import { SubPanel } from "components/Containers/SubPanel";
import { ThumbnailHeader } from "components/ThumbnailHeader";
import { HorizontalLine } from "components/HorizontalLine";
import { GetChroniclesChaptersQuery } from "graphql/generated";
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { getDescription } from "helpers/description";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
chronicle: ChronicleWithTranslations;
chapters: NonNullable<GetChroniclesChaptersQuery["chroniclesChapters"]>["data"];
}
const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: chronicle.translations,
languageExtractor: useCallback(
(item: ChronicleWithTranslations["translations"][number]) =>
item?.language?.data?.attributes?.code,
[]
),
});
const primaryContent = useMemo<
NonNullable<ChronicleWithTranslations["contents"]>["data"][number]["attributes"]
>(
() =>
filterHasAttributes(chronicle.contents?.data, ["attributes.translations"] as const)[0]
?.attributes,
[chronicle.contents?.data]
);
const [selectedContentTranslation, ContentLanguageSwitcher, ContentLanguageSwitcherProps] =
useSmartLanguage({
items: primaryContent?.translations ?? [],
languageExtractor: useCallback(
(
item: NonNullable<
NonNullable<
NonNullable<ChronicleWithTranslations["contents"]>["data"][number]["attributes"]
>["translations"]
>[number]
) => item?.language?.data?.attributes?.code,
[]
),
});
const contentPanel = useMemo(
() => (
<ContentPanel>
<ReturnButton
displayOnlyOn={"1ColumnLayout"}
href="/chronicles"
title={langui.chronicles}
className="mb-10"
/>
{isDefined(selectedTranslation) ? (
<>
<h1 className="mb-16 text-center text-3xl">{selectedTranslation.title}</h1>
{languageSwitcherProps.locales.size > 1 && (
<LanguageSwitcher {...languageSwitcherProps} />
)}
{isDefined(selectedTranslation.body) && (
<Markdawn text={selectedTranslation.body.body} />
)}
</>
) : (
<>
{selectedContentTranslation && (
<>
<ThumbnailHeader
pre_title={selectedContentTranslation.pre_title}
title={selectedContentTranslation.title}
subtitle={selectedContentTranslation.subtitle}
languageSwitcher={
ContentLanguageSwitcherProps.locales.size > 1 ? (
<ContentLanguageSwitcher {...ContentLanguageSwitcherProps} />
) : undefined
}
categories={primaryContent?.categories}
type={primaryContent?.type}
description={selectedContentTranslation.description}
thumbnail={primaryContent?.thumbnail?.data?.attributes}
/>
{selectedContentTranslation.text_set?.text && (
<>
<HorizontalLine />
<Markdawn text={selectedContentTranslation.text_set.text} />
</>
)}
</>
)}
</>
)}
</ContentPanel>
),
[
selectedTranslation,
languageSwitcherProps,
LanguageSwitcher,
selectedContentTranslation,
ContentLanguageSwitcherProps,
ContentLanguageSwitcher,
primaryContent?.categories,
primaryContent?.type,
primaryContent?.thumbnail?.data?.attributes,
langui,
]
);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
displayOnlyOn={"3ColumnsLayout"}
href="/chronicles"
title={langui.chronicles}
/>
<HorizontalLine />
<div className="grid gap-16">
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map(
(chapter) => (
<TranslatedChroniclesList
key={chapter.id}
chronicles={chapter.attributes.chronicles.data}
translations={filterHasAttributes(chapter.attributes.titles, [
"language.data.attributes.code",
] as const).map((translation) => ({
title: translation.title,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(chapter.attributes.slug) }}
currentSlug={chronicle.slug}
/>
)
)}
</div>
</SubPanel>
),
[chapters, chronicle.slug, langui]
);
return (
<AppLayout
contentPanel={contentPanel}
subPanel={subPanel}
subPanelIcon={Icon.FormatListNumbered}
{...otherProps}
/>
);
};
export default Chronicle;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const slug =
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
const chronicle = await sdk.getChronicle({
language_code: context.locale ?? "en",
slug: slug,
});
const chronicles = await sdk.getChroniclesChapters();
if (
!chronicle.chronicles?.data[0]?.attributes?.translations ||
!chronicles.chroniclesChapters?.data
)
return { notFound: true };
const { title, description } = (() => {
if (context.locale && context.locales) {
if (chronicle.chronicles.data[0].attributes.contents?.data[0]?.attributes?.translations) {
const selectedContentTranslation = staticSmartLanguage({
items: chronicle.chronicles.data[0].attributes.contents.data[0].attributes.translations,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
});
if (selectedContentTranslation) {
return {
title: prettyInlineTitle(
selectedContentTranslation.pre_title,
selectedContentTranslation.title,
selectedContentTranslation.subtitle
),
description: getDescription(selectedContentTranslation.description, {
[langui.type ?? "Type"]: [
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.type?.data
?.attributes?.titles?.[0]?.title,
],
[langui.categories ?? "Categories"]: filterHasAttributes(
chronicle.chronicles.data[0].attributes.contents.data[0].attributes.categories
?.data,
["attributes"] as const
).map((category) => category.attributes.short),
}),
};
}
} else {
const selectedTranslation = staticSmartLanguage({
items: chronicle.chronicles.data[0].attributes.translations,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
});
if (selectedTranslation) {
return {
title: selectedTranslation.title,
description: selectedTranslation.summary,
};
}
}
}
return {
title: prettySlug(chronicle.chronicles.data[0].attributes.slug),
description: undefined,
};
})();
const thumbnail =
chronicle.chronicles.data[0].attributes.translations.length === 0
? chronicle.chronicles.data[0].attributes.contents?.data[0]?.attributes?.thumbnail?.data
?.attributes
: undefined;
const props: Props = {
chronicle: chronicle.chronicles.data[0].attributes as ChronicleWithTranslations,
chapters: chronicles.chroniclesChapters.data,
openGraph: getOpenGraph(langui, title, description, thumbnail),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getChroniclesSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(contents.chronicles?.data, ["attributes"] as const).map((wikiPage) => {
context.locales?.map((local) =>
paths.push({
params: { slug: wikiPage.attributes.slug },
locale: local,
})
);
});
return {
paths,
fallback: "blocking",
};
};

View File

@ -1,84 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getReadySdk } from "graphql/sdk";
import { GetChroniclesChaptersQuery } from "graphql/generated";
import { filterHasAttributes } from "helpers/others";
import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
chapters: NonNullable<GetChroniclesChaptersQuery["chroniclesChapters"]>["data"];
}
const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.WatchLater}
title={langui.chronicles}
description={langui.chronicles_description}
/>
<HorizontalLine />
<div className="grid gap-16">
{filterHasAttributes(chapters, ["attributes.chronicles", "id"] as const).map(
(chapter) => (
<TranslatedChroniclesList
key={chapter.id}
chronicles={chapter.attributes.chronicles.data}
translations={filterHasAttributes(chapter.attributes.titles, [
"language.data.attributes.code",
] as const).map((translation) => ({
title: translation.title,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(chapter.attributes.slug) }}
/>
)
)}
</div>
</SubPanel>
),
[chapters, langui]
);
return <AppLayout subPanel={subPanel} {...otherProps} />;
};
export default Chronicles;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const chronicles = await sdk.getChroniclesChapters();
if (!chronicles.chroniclesChapters?.data) return { notFound: true };
const props: Props = {
chapters: chronicles.chroniclesChapters.data,
openGraph: getOpenGraph(langui, langui.chronicles ?? "Chronicles"),
};
return {
props: props,
};
};

View File

@ -1,525 +0,0 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { Fragment, useCallback, useMemo } from "react";
import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip";
import { HorizontalLine } from "components/HorizontalLine";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { Markdawn, TableOfContents } from "components/Markdown/Markdawn";
import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import { RecorderChip } from "components/RecorderChip";
import { ThumbnailHeader } from "components/ThumbnailHeader";
import { ToolTip } from "components/ToolTip";
import { getReadySdk } from "graphql/sdk";
import {
prettyInlineTitle,
prettyLanguage,
prettyItemSubType,
prettySlug,
} from "helpers/formatters";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { filterHasAttributes, getStatusDescription, isDefinedAndNotEmpty } from "helpers/others";
import { ContentWithTranslations } from "types/types";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { getOpenGraph } from "helpers/openGraph";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { getDescription } from "helpers/description";
import { TranslatedPreviewLine } from "components/PreviewLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
content: ContentWithTranslations;
}
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
const isContentPanelAtLeast2xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast2xl);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const languages = useAtomGetter(atoms.localData.languages);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: content.translations,
languageExtractor: useCallback(
(item: NonNullable<Props["content"]["translations"][number]>) =>
item.language?.data?.attributes?.code,
[]
),
});
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
const { previousContent, nextContent } = useMemo(
() => ({
previousContent:
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
? getPreviousContent(content.folder.data.attributes.contents.data, content.slug)
: undefined,
nextContent:
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
? getNextContent(content.folder.data.attributes.contents.data, content.slug)
: undefined,
}),
[content.folder, content.slug]
);
const returnButtonProps = useMemo(
() => ({
href: content.folder?.data?.attributes
? `/contents/folder/${content.folder.data.attributes.slug}`
: "/contents",
translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
language: title.language.data.attributes.code,
title: title.title,
})),
fallback: {
title: content.folder?.data?.attributes
? prettySlug(content.folder.data.attributes.slug)
: langui.contents,
},
langui,
}),
[content.folder?.data?.attributes, langui]
);
const subPanel = useMemo(
() => (
<SubPanel>
<TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
{selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
<>
<HorizontalLine />
<div className="grid gap-5">
<h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes.code ===
selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2>
{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 font-bold">{langui.source_language}:</p>
<Chip
text={prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes.code,
languages
)}
/>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p>
<ToolTip
content={getStatusDescription(selectedTranslation.text_set.status, langui)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.text_set.status} />
</ToolTip>
</div>
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.transcribers}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
"attributes",
"id",
] as const).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip recorder={recorder.attributes} />
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.translators}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
"attributes",
"id",
] as const).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip recorder={recorder.attributes} />
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.proofreaders}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
"attributes",
"id",
] as const).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip recorder={recorder.attributes} />
</Fragment>
))}
</div>
</div>
)}
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers font-bold">{langui.notes}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn text={selectedTranslation.text_set.notes} />
</div>
</div>
)}
</div>
</>
)}
{selectedTranslation?.text_set?.text && (
<>
<TableOfContents
text={selectedTranslation.text_set.text}
title={prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
horizontalLine
/>
</>
)}
{content.ranged_contents?.data && content.ranged_contents.data.length > 0 && (
<>
<HorizontalLine />
<div>
<p className="font-headers text-2xl font-bold">{langui.source}</p>
<div className="mt-6 grid place-items-center gap-6">
{filterHasAttributes(content.ranged_contents.data, [
"attributes.library_item.data.attributes",
"attributes.library_item.data.id",
] as const).map((rangedContent) => {
const libraryItem = rangedContent.attributes.library_item.data;
return (
<div
key={libraryItem.attributes.slug}
className={cIf(is1ColumnLayout, "w-3/4")}>
<PreviewCard
href={`/library/${libraryItem.attributes.slug}`}
title={libraryItem.attributes.title}
subtitle={libraryItem.attributes.subtitle}
thumbnail={libraryItem.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7"
thumbnailRounded={false}
topChips={
libraryItem.attributes.metadata &&
libraryItem.attributes.metadata.length > 0 &&
libraryItem.attributes.metadata[0]
? [prettyItemSubType(libraryItem.attributes.metadata[0])]
: []
}
bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [
"attributes",
] as const).map((category) => category.attributes.short)}
metadata={{
releaseDate: libraryItem.attributes.release_date,
price: libraryItem.attributes.price,
position: "Bottom",
}}
infoAppend={
!isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && (
<PreviewCardCTAs id={libraryItem.id} />
)
}
/>
</div>
);
})}
</div>
</div>
</>
)}
</SubPanel>
),
[
content.ranged_contents?.data,
languages,
langui,
returnButtonProps,
selectedTranslation,
is1ColumnLayout,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel>
<TranslatedReturnButton
{...returnButtonProps}
displayOnlyOn="1ColumnLayout"
className="mb-10"
/>
<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}
languageSwitcher={
languageSwitcherProps.locales.size > 1 ? (
<LanguageSwitcher {...languageSwitcherProps} />
) : undefined
}
/>
{previousContent?.attributes && (
<div className="mt-12 mb-8 w-full">
<h2 className="mb-4 text-center text-2xl">{langui.previous_content}</h2>
<TranslatedPreviewLine
href={`/contents/${previousContent.attributes.slug}`}
translations={filterHasAttributes(previousContent.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
pre_title: translation.pre_title,
title: translation.title,
subtitle: translation.subtitle,
language: translation.language.data.attributes.code,
}))}
fallback={{
title: prettySlug(previousContent.attributes.slug),
}}
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
topChips={
isContentPanelAtLeast2xl && 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={
isContentPanelAtLeast2xl
? previousContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
: undefined
}
/>
</div>
)}
{selectedTranslation?.text_set?.text && (
<>
<HorizontalLine />
<Markdawn text={selectedTranslation.text_set.text} />
</>
)}
{nextContent?.attributes && (
<>
<HorizontalLine />
<h2 className="mb-4 text-center text-2xl">{langui.followup_content}</h2>
<TranslatedPreviewLine
href={`/contents/${nextContent.attributes.slug}`}
translations={filterHasAttributes(nextContent.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
pre_title: translation.pre_title,
title: translation.title,
subtitle: translation.subtitle,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: nextContent.attributes.slug }}
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
topChips={
isContentPanelAtLeast2xl && 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={
isContentPanelAtLeast2xl
? nextContent.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)
: undefined
}
/>
</>
)}
</div>
</ContentPanel>
),
[
LanguageSwitcher,
content.categories,
content.thumbnail?.data?.attributes,
content.type,
isContentPanelAtLeast2xl,
languageSwitcherProps,
langui,
nextContent?.attributes,
previousContent?.attributes,
returnButtonProps,
selectedTranslation?.description,
selectedTranslation?.pre_title,
selectedTranslation?.subtitle,
selectedTranslation?.text_set?.text,
selectedTranslation?.title,
]
);
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
};
export default Content;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const slug = context.params?.slug ? context.params.slug.toString() : "";
const content = await sdk.getContentText({
slug: slug,
language_code: context.locale ?? "en",
});
if (!content.contents?.data[0]?.attributes?.translations) {
return { notFound: true };
}
const { title, description } = (() => {
if (context.locale && context.locales) {
const selectedTranslation = staticSmartLanguage({
items: content.contents.data[0].attributes.translations,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
});
if (selectedTranslation) {
return {
title: prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
),
description: getDescription(selectedTranslation.description, {
[langui.type ?? "Type"]: [
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
],
[langui.categories ?? "Categories"]: filterHasAttributes(
content.contents.data[0].attributes.categories?.data,
["attributes"] as const
).map((category) => category.attributes.short),
}),
};
}
}
return {
title: prettySlug(content.contents.data[0].attributes.slug),
description: undefined,
};
})();
const thumbnail = content.contents.data[0].attributes.thumbnail?.data?.attributes;
content.contents.data[0].attributes.folder?.data?.attributes?.contents?.data.sort((a, b) =>
a.attributes && b.attributes ? naturalCompare(a.attributes.slug, b.attributes.slug) : 0
);
const props: Props = {
content: content.contents.data[0].attributes as ContentWithTranslations,
openGraph: getOpenGraph(langui, title, description, thumbnail),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getContentsSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(contents.contents?.data, ["attributes"] as const).map((item) => {
context.locales?.map((local) => {
paths.push({
params: { slug: item.attributes.slug },
locale: local,
});
});
});
return {
paths,
fallback: "blocking",
};
};
/*
*
* PRIVATE METHODS
*/
type FolderContents = NonNullable<
NonNullable<
NonNullable<NonNullable<ContentWithTranslations["folder"]>["data"]>["attributes"]
>["contents"]
>["data"];
const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (content.attributes?.slug === currentSlug && index > 0) {
return contents[index - 1];
}
}
return undefined;
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const getNextContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (content.attributes?.slug === currentSlug && index < contents.length - 1) {
return contents[index + 1];
}
}
return undefined;
};

View File

@ -1,315 +0,0 @@
import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react";
import { useBoolean } from "usehooks-ts";
import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Select } from "components/Inputs/Select";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { getReadySdk } from "graphql/sdk";
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { Icon } from "components/Ico";
import { filterDefined, filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import { GetContentsQuery } from "graphql/generated";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { cJoin, cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
groupingMethod: -1,
keepInfoVisible: false,
searchName: "",
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
contents: NonNullable<GetContentsQuery["contents"]>["data"];
}
const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod
);
const {
value: keepInfoVisible,
toggle: toggleKeepInfoVisible,
setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const groupingFunction = useCallback(
(
item: SelectiveNonNullable<
NonNullable<GetContentsQuery["contents"]>["data"][number],
"attributes" | "id"
>
): string[] => {
switch (groupingMethod) {
case 0: {
const categories = filterHasAttributes(item.attributes.categories?.data, [
"attributes",
] as const);
if (categories.length > 0) {
return categories.map((category) => category.attributes.name);
}
return [langui.no_category ?? "No category"];
}
case 1: {
return [
item.attributes.type?.data?.attributes?.titles?.[0]?.title ??
item.attributes.type?.data?.attributes?.slug
? prettySlug(item.attributes.type.data.attributes.slug)
: langui.no_type ?? "No type",
];
}
default: {
return [""];
}
}
},
[groupingMethod, langui]
);
const filteringFunction = useCallback(
(item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">) => {
if (searchName.length > 1) {
if (
filterDefined(item.attributes.translations).find((translation) =>
prettyInlineTitle(translation.pre_title, translation.title, translation.subtitle)
.toLowerCase()
.includes(searchName.toLowerCase())
)
) {
return true;
}
return false;
}
return true;
},
[searchName]
);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Workspaces}
title={langui.contents}
description={langui.contents_description}
/>
<HorizontalLine />
<Button href="/contents" text={langui.switch_to_folder_view} icon={Icon.Folder} />
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={(name) => {
setSearchName(name);
if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Contents/All", "Change search term");
} else {
sendAnalytics("Contents/All", "Clear search term");
}
}}
/>
<WithLabel label={langui.group_by}>
<Select
className="w-full"
options={[langui.category ?? "Category", langui.type ?? "Type"]}
value={groupingMethod}
onChange={(value) => {
setGroupingMethod(value);
sendAnalytics(
"Contents/All",
`Change grouping method (${["none", "category", "type"][value + 1]})`
);
}}
allowEmpty
/>
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch
value={keepInfoVisible}
onClick={() => {
toggleKeepInfoVisible();
sendAnalytics("Contents/All", `Always ${keepInfoVisible ? "hide" : "show"} info`);
}}
/>
</WithLabel>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
sendAnalytics("Contents/All", "Reset all filters");
}}
/>
</SubPanel>
),
[
groupingMethod,
hoverable,
keepInfoVisible,
langui.always_show_info,
langui.category,
langui.contents,
langui.contents_description,
langui.group_by,
langui.reset_all_filters,
langui.search_title,
langui.switch_to_folder_view,
langui.type,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(contents, ["attributes", "id"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<TranslatedPreviewCard
href={`/contents/${item.attributes.slug}`}
translations={filterHasAttributes(item.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
pre_title: translation.pre_title,
title: translation.title,
subtitle: translation.subtitle,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.attributes.slug) }}
thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
thumbnailForceAspectRatio
topChips={
item.attributes.type?.data?.attributes
? [
item.attributes.type.data.attributes.titles?.[0]
? item.attributes.type.data.attributes.titles[0]?.title
: prettySlug(item.attributes.type.data.attributes.slug),
]
: undefined
}
bottomChips={item.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
keepInfoVisible={keepInfoVisible}
/>
)}
className={cJoin(
"items-end",
cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5"
)
)}
groupingFunction={groupingFunction}
filteringFunction={filteringFunction}
searchingTerm={searchName}
searchingBy={(item) =>
`
${item.attributes.slug}
${filterDefined(item.attributes.translations)
.map((translation) =>
prettyInlineTitle(translation.pre_title, translation.title, translation.subtitle)
)
.join(" ")}`
}
paginationItemPerPage={50}
/>
</ContentPanel>
),
[
isContentPanelAtLeast4xl,
contents,
filteringFunction,
groupingFunction,
keepInfoVisible,
searchName,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
{...otherProps}
/>
);
};
export default Contents;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const contents = await sdk.getContents({
language_code: context.locale ?? "en",
});
if (!contents.contents) return { notFound: true };
contents.contents.data.sort((a, b) => {
const titleA = a.attributes?.slug ?? "";
const titleB = b.attributes?.slug ?? "";
return naturalCompare(titleA, titleB);
});
const props: Props = {
contents: contents.contents.data,
openGraph: getOpenGraph(langui, langui.contents ?? "Contents"),
};
return {
props: props,
};
};

View File

@ -1,286 +0,0 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo } from "react";
import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes } from "helpers/others";
import { GetContentsFolderQuery } from "graphql/generated";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { prettySlug } from "helpers/formatters";
import { SmartList } from "components/SmartList";
import { Ico, Icon } from "components/Ico";
import { Button, TranslatedButton } from "components/Inputs/Button";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
import { cJoin, cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
folder: NonNullable<
NonNullable<GetContentsFolderQuery["contentsFolders"]>["data"][number]["attributes"]
>;
}
const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Workspaces}
title={langui.contents}
description={langui.contents_description}
/>
<HorizontalLine />
<Button href="/contents/all" text={langui.switch_to_grid_view} icon={Icon.Apps} />
</SubPanel>
),
[langui.contents, langui.contents_description, langui.switch_to_grid_view]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
{folder.parent_folder?.data?.attributes && (
<>
{folder.parent_folder.data.attributes.slug === "root" ? (
<Button href="/contents" icon={Icon.Home} />
) : (
<TranslatedButton
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
translations={filterHasAttributes(folder.parent_folder.data.attributes.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
language: title.language.data.attributes.code,
text: title.title,
}))}
fallback={{
text: prettySlug(folder.parent_folder.data.attributes.slug),
}}
/>
)}
<Ico icon={Icon.ChevronRight} />
</>
)}
{folder.slug === "root" ? (
<Button href="/contents" icon={Icon.Home} active />
) : (
<TranslatedButton
translations={filterHasAttributes(folder.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
language: title.language.data.attributes.code,
text: title.title,
}))}
fallback={{
text: prettySlug(folder.slug),
}}
active
/>
)}
</div>
<SmartList
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<TranslatedPreviewFolder
href={`/contents/folder/${item.attributes.slug}`}
translations={filterHasAttributes(item.attributes.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
title: title.title,
language: title.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.attributes.slug) }}
/>
)}
className={cJoin(
"items-end",
cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-4"
)
)}
renderWhenEmpty={() => <></>}
groupingFunction={() => [langui.folders ?? "Folders"]}
/>
<SmartList
items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<TranslatedPreviewCard
href={`/contents/${item.attributes.slug}`}
translations={filterHasAttributes(item.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
pre_title: translation.pre_title,
title: translation.title,
subtitle: translation.subtitle,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.attributes.slug) }}
thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
thumbnailForceAspectRatio
topChips={
item.attributes.type?.data?.attributes
? [
item.attributes.type.data.attributes.titles?.[0]
? item.attributes.type.data.attributes.titles[0]?.title
: prettySlug(item.attributes.type.data.attributes.slug),
]
: undefined
}
bottomChips={item.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
keepInfoVisible
/>
)}
className={cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5"
)}
renderWhenEmpty={() => <></>}
groupingFunction={() => [langui.contents ?? "Contents"]}
/>
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
<NoContentNorFolderMessage />
)}
</ContentPanel>
),
[
folder.contents?.data,
folder.parent_folder?.data?.attributes,
folder.slug,
folder.subfolders?.data,
folder.titles,
isContentPanelAtLeast4xl,
langui,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
openGraph={openGraph}
{...otherProps}
/>
);
};
export default ContentsFolder;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const slug = context.params?.slug ? context.params.slug.toString() : "";
const contentsFolder = await sdk.getContentsFolder({
slug: slug,
language_code: context.locale ?? "en",
});
if (!contentsFolder.contentsFolders?.data[0]?.attributes) {
return { notFound: true };
}
const folder = contentsFolder.contentsFolders.data[0].attributes;
folder.subfolders?.data.sort((a, b) =>
a.attributes && b.attributes ? naturalCompare(a.attributes.slug, b.attributes.slug) : 0
);
folder.contents?.data.sort((a, b) =>
a.attributes && b.attributes ? naturalCompare(a.attributes.slug, b.attributes.slug) : 0
);
const title = (() => {
if (slug === "root") {
return langui.contents ?? "Contents";
}
if (context.locale && context.locales) {
const selectedTranslation = staticSmartLanguage({
items: folder.titles,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
});
if (selectedTranslation) {
return selectedTranslation.title;
}
}
return prettySlug(folder.slug);
})();
const props: Props = {
openGraph: getOpenGraph(langui, title),
folder,
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getContentsFoldersSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(contents.contentsFolders?.data, ["attributes"] as const).map((item) => {
context.locales?.map((local) => {
paths.push({
params: { slug: item.attributes.slug },
locale: local,
});
});
});
return {
paths,
fallback: "blocking",
};
};
/*
*
* PRIVATE COMPONENTS
*/
const NoContentNorFolderMessage = () => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<div className="grid place-content-center">
<div
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
border-dark p-8 text-dark opacity-40">
<p className="max-w-xs text-2xl">{langui.empty_folder_message}</p>
</div>
</div>
);
};

View File

@ -1,22 +0,0 @@
import { GetStaticProps } from "next";
import ContentsFolder, { getStaticProps as folderGetStaticProps } from "./folder/[slug]";
/*
*
* PAGE
*/
const Contents = (props: Parameters<typeof ContentsFolder>[0]): JSX.Element => (
<ContentsFolder {...props} />
);
export default Contents;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
context.params = { slug: "root" };
return await folderGetStaticProps(context);
};

View File

@ -1,264 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip";
import { Button } from "components/Inputs/Button";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { ToolTip } from "components/ToolTip";
import { DevGetContentsQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { Report, Severity } from "types/Report";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { sJoin } from "helpers/formatters";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
contents: DevGetContentsQuery;
}
const CheckupContents = ({ contents, ...otherProps }: Props): JSX.Element => {
const testReport = useMemo(() => testingContent(contents), [contents]);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p />
<p />
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
{testReport.lines
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => b.severity - a.severity)
.map((line, index) => (
<div
key={index}
className="mb-2 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center
justify-items-start gap-2">
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip text={line.type} />
<Chip
className={
line.severity === Severity.VeryHigh
? "bg-[#f00] font-bold !opacity-100"
: line.severity === Severity.High
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === Severity.Medium
? "bg-[#fff344] !opacity-100"
: ""
}
text={Severity[line.severity]}
/>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
</div>
))}
</ContentPanel>
),
[testReport.lines, testReport.title]
);
return <AppLayout contentPanel={contentPanel} {...otherProps} />;
};
export default CheckupContents;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const contents = await sdk.devGetContents();
const props: Props = {
contents: contents,
openGraph: getOpenGraph(langui, "Checkup Contents"),
};
return {
props: props,
};
};
/*
*
* PRIVATE METHODS
*/
const testingContent = (contents: Props["contents"]): Report => {
const report: Report = {
title: "Contents",
lines: [],
};
filterHasAttributes(contents.contents?.data, ["attributes"] as const).map((content) => {
const backendUrl = sJoin(
process.env.NEXT_PUBLIC_URL_CMS,
"/admin/content-manager/collectionType/api::content.content/",
content.id
);
const frontendUrl = sJoin(
process.env.NEXT_PUBLIC_URL_SELF,
"/contents/",
content.attributes.slug
);
if (content.attributes.categories?.data.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Category",
type: "Missing",
severity: Severity.High,
description: "The Content has no Category.",
recommandation: "Select a Category in relation with the Content",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!content.attributes.type?.data?.id) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Type",
type: "Missing",
severity: Severity.High,
description: "The Content has no Type.",
recommandation: 'If unsure, use the "Other" Type.',
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (content.attributes.ranged_contents?.data.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Ranged Content",
type: "Improvement",
severity: Severity.Low,
description: "The Content has no Ranged Content.",
recommandation:
"If this Content is available in one or multiple Library Item(s),\
create a Range Content to connect the Content to its Library Item(s).",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!content.attributes.thumbnail?.data?.id) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Thumbnail",
type: "Missing",
severity: Severity.High,
description: "The Content has no Thumbnail.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (content.attributes.translations?.length === 0) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Titles",
type: "Missing",
severity: Severity.High,
description: "The Content has no Titles.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
const titleLanguages: string[] = [];
if (content.attributes.translations && content.attributes.translations.length > 0) {
filterDefined(content.attributes.translations).map((translation, titleIndex) => {
if (translation.language?.data?.id) {
if (translation.language.data.id in titleLanguages) {
report.lines.push({
subitems: [content.attributes.slug, `Title ${titleIndex.toString()}`],
name: "Duplicate Language",
type: "Error",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
titleLanguages.push(translation.language.data.id);
}
} else {
report.lines.push({
subitems: [content.attributes.slug, `Title ${titleIndex.toString()}`],
name: "No Language",
type: "Error",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!translation.description) {
report.lines.push({
subitems: [content.attributes.slug, `Title ${titleIndex.toString()}`],
name: "No Description",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!translation.text_set) {
report.lines.push({
subitems: [
content.attributes.slug,
translation.language?.data?.attributes?.code ?? "",
],
name: "No Text Set",
type: "Missing",
severity: Severity.High,
description: "The Content has no Text Set.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
});
} else {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Translations",
type: "Missing",
severity: Severity.High,
description: "The Content has no Translations.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
});
return report;
};

View File

@ -1,654 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip";
import { Button } from "components/Inputs/Button";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { ToolTip } from "components/ToolTip";
import {
DevGetLibraryItemsQuery,
Enum_Componentcollectionscomponentlibraryimages_Status,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { Report, Severity } from "types/Report";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { sJoin } from "helpers/formatters";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
libraryItems: DevGetLibraryItemsQuery;
}
const CheckupLibraryItems = ({ libraryItems, ...otherProps }: Props): JSX.Element => {
const testReport = testingLibraryItem(libraryItems);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
{<h2 className="text-2xl">{testReport.title}</h2>}
<div className="my-4 grid grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center gap-2">
<p />
<p />
<p className="font-headers">Ref</p>
<p className="font-headers">Name</p>
<p className="font-headers">Type</p>
<p className="font-headers">Severity</p>
<p className="font-headers">Description</p>
</div>
{testReport.lines
.sort((a, b) => a.name.localeCompare(b.name))
.sort((a, b) => b.severity - a.severity)
.map((line, index) => (
<div
key={index}
className="mb-2 grid
grid-cols-[2em,3em,2fr,1fr,0.5fr,0.5fr,2fr] items-center justify-items-start gap-2">
<Button href={line.frontendUrl} className="w-4 text-xs" text="F" alwaysNewTab />
<Button href={line.backendUrl} className="w-4 text-xs" text="B" alwaysNewTab />
<p>{line.subitems.join(" -> ")}</p>
<p>{line.name}</p>
<Chip text={line.type} />
<Chip
className={
line.severity === Severity.VeryHigh
? "bg-[#f00] font-bold !opacity-100"
: line.severity === Severity.High
? "bg-[#ff6600] font-bold !opacity-100"
: line.severity === Severity.Medium
? "bg-[#fff344] !opacity-100"
: ""
}
text={Severity[line.severity]}
/>
<ToolTip content={line.recommandation} placement="left">
<p>{line.description}</p>
</ToolTip>
</div>
))}
</ContentPanel>
),
[testReport.lines, testReport.title]
);
return <AppLayout contentPanel={contentPanel} {...otherProps} />;
};
export default CheckupLibraryItems;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const libraryItems = await sdk.devGetLibraryItems();
const props: Props = {
libraryItems: libraryItems,
openGraph: getOpenGraph(langui, "Checkup Library Items"),
};
return {
props: props,
};
};
/*
*
* PRIVATE METHODS
*/
const testingLibraryItem = (libraryItems: Props["libraryItems"]): Report => {
const report: Report = {
title: "Contents",
lines: [],
};
libraryItems.libraryItems?.data.map((item) => {
if (item.attributes) {
const backendUrl = sJoin(
process.env.NEXT_PUBLIC_URL_CMS,
"/admin/content-manager/collectionType/api::library-item.library-item/",
item.id
);
const frontendUrl = sJoin(
process.env.NEXT_PUBLIC_URL_SELF,
"/library/",
item.attributes.slug
);
if (item.attributes.categories?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug],
name: "No Category",
type: "Missing",
severity: Severity.High,
description: "The Item has no Category.",
recommandation: "Select a Category in relation with the Item",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!item.attributes.root_item && item.attributes.subitem_of?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug],
name: "Disconnected Item",
type: "Error",
severity: Severity.VeryHigh,
description: "The Item is neither a Root Item, nor is it a subitem of another item.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (item.attributes.contents?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug],
name: "No Contents",
type: "Missing",
severity: Severity.Low,
description: "The Item has no Contents.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!item.attributes.thumbnail?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug],
name: "No Thumbnail",
type: "Missing",
severity: Severity.High,
description: "The Item has no Thumbnail.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (item.attributes.images?.length === 0) {
report.lines.push({
subitems: [item.attributes.slug],
name: "No Images",
type: "Missing",
severity: Severity.Low,
description: "The Item has no Images.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
item.attributes.images?.map((image, imageIndex) => {
const imagesLanguages: string[] = [];
if (image && item.attributes) {
if (image.language?.data?.id) {
if (image.language.data.id in imagesLanguages) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "Duplicate Language",
type: "Error",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
imagesLanguages.push(image.language.data.id);
}
} else {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Language",
type: "Error",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.source_language?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Source Language",
type: "Error",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.status !== Enum_Componentcollectionscomponentlibraryimages_Status.Done) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "Not Done Status",
type: "Improvement",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.source_language?.data?.id === image.language?.data?.id) {
if (image.scanners?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Scanners",
type: "Missing",
severity: Severity.High,
description: "The Item is a Scan but doesn't credit any Scanners.",
recommandation: "Add the appropriate Scanners.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.cleaners?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Cleaners",
type: "Missing",
severity: Severity.High,
description: "The Item is a Scan but doesn't credit any Cleaners.",
recommandation: "Add the appropriate Cleaners.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.typesetters?.data && image.typesetters.data.length > 0) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "Credited Typesetters",
type: "Error",
severity: Severity.High,
description: "The Item is a Scan but credits one or more Typesetters.",
recommandation:
"If appropriate, create a Scanlation Images Set\
with the Typesetters credited there.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
} else {
if (image.typesetters?.data.length === 0) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Typesetters",
type: "Missing",
severity: Severity.High,
description: "The Item is a Scanlation but doesn't credit any Typesetters.",
recommandation: "Add the appropriate Typesetters.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.scanners?.data && image.scanners.data.length > 0) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "Credited Scanners",
type: "Error",
severity: Severity.High,
description: "The Item is a Scanlation but credits one or more Scanners.",
recommandation:
"If appropriate, create a Scanners Images Set\
with the Scanners credited there.",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
if (image.cover) {
if (!image.cover.front?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Cover"],
name: "No Front",
type: "Missing",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.cover.spine?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Cover"],
name: "No spine",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.cover.back?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Cover"],
name: "No Back",
type: "Missing",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.cover.full?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Cover"],
name: "No Full",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
} else {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Cover",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.dust_jacket) {
if (!image.dust_jacket.front?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No Front",
type: "Missing",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.dust_jacket.spine?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No spine",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.dust_jacket.back?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No Back",
type: "Missing",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.dust_jacket.full?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No Full",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.dust_jacket.flap_front?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No Flap Front",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.dust_jacket.flap_back?.data?.id) {
report.lines.push({
subitems: [
item.attributes.slug,
`Images ${imageIndex.toString()}`,
"Dust Jacket",
],
name: "No Flap Back",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
} else {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Dust Jacket",
type: "Missing",
severity: Severity.VeryLow,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (image.obi_belt) {
if (!image.obi_belt.front?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No Front",
type: "Missing",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.obi_belt.spine?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No spine",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.obi_belt.back?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No Back",
type: "Missing",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.obi_belt.full?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No Full",
type: "Missing",
severity: Severity.Low,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.obi_belt.flap_front?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No Flap Front",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (!image.obi_belt.flap_back?.data?.id) {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`, "Obi Belt"],
name: "No Flap Back",
type: "Missing",
severity: Severity.Medium,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
} else {
report.lines.push({
subitems: [item.attributes.slug, `Images ${imageIndex.toString()}`],
name: "No Obi Belt",
type: "Missing",
severity: Severity.VeryLow,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
});
}
if (item.attributes.descriptions && item.attributes.descriptions.length > 0) {
const descriptionLanguages: string[] = [];
item.attributes.descriptions.map((description, descriptionIndex) => {
if (description && item.attributes) {
if (description.description.length < 10) {
report.lines.push({
subitems: [item.attributes.slug, `Description ${descriptionIndex}`],
name: "No Text",
type: "Missing",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (description.language?.data?.id) {
if (description.language.data.id in descriptionLanguages) {
report.lines.push({
subitems: [item.attributes.slug, `Description ${descriptionIndex}`],
name: "Duplicate Language",
type: "Error",
severity: Severity.High,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
descriptionLanguages.push(description.language.data.id);
}
} else {
report.lines.push({
subitems: [item.attributes.slug, `Description ${descriptionIndex}`],
name: "No Language",
type: "Error",
severity: Severity.VeryHigh,
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
});
} else {
report.lines.push({
subitems: [item.attributes.slug],
name: "No Description",
type: "Missing",
severity: Severity.Medium,
description: "The Item has no Description.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (item.attributes.urls?.length === 0) {
report.lines.push({
subitems: [item.attributes.slug],
name: "No URLs",
type: "Missing",
severity: Severity.VeryLow,
description: "The Item has no URLs.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
}
});
return report;
};

View File

@ -1,422 +0,0 @@
import { GetStaticProps } from "next";
import { useCallback, useMemo, useRef, useState } from "react";
import TurndownService from "turndown";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Button } from "components/Inputs/Button";
import { Markdawn, TableOfContents } from "components/Markdown/Markdawn";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { Popup } from "components/Containers/Popup";
import { ToolTip } from "components/ToolTip";
import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const Editor = (props: Props): JSX.Element => {
const handleInput = useCallback((text: string) => {
setMarkdown(text);
}, []);
const [markdown, setMarkdown] = useState("");
const [converterOpened, setConverterOpened] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const transformationWrapper = useCallback(
(
transformation: (
value: string,
selectionStart: number,
selectedEnd: number
) => { prependLength: number; transformedValue: string }
) => {
if (textAreaRef.current) {
const { value, selectionStart, selectionEnd } = textAreaRef.current;
const { prependLength, transformedValue } = transformation(
value,
selectionStart,
selectionEnd
);
textAreaRef.current.value = transformedValue;
handleInput(textAreaRef.current.value);
textAreaRef.current.focus();
textAreaRef.current.selectionStart = selectionStart + prependLength;
textAreaRef.current.selectionEnd = selectionEnd + prependLength;
}
},
[handleInput]
);
const wrap = useCallback(
(wrapper: string, properties?: Record<string, string>, addInnerNewLines?: boolean) => {
transformationWrapper((value, selectionStart, selectionEnd) => {
let prepend = wrapper;
let append = wrapper;
if (properties) {
prepend = `<${wrapper}${Object.entries(properties).map(
([propertyName, propertyValue]) => ` ${propertyName}="${propertyValue}"`
)}>`;
append = `</${wrapper}>`;
}
if (addInnerNewLines === true) {
prepend = `${prepend}\n`;
append = `\n${append}`;
}
let newValue = "";
newValue += value.slice(0, selectionStart);
newValue += prepend;
newValue += value.slice(selectionStart, selectionEnd);
newValue += append;
newValue += value.slice(selectionEnd);
return { prependLength: prepend.length, transformedValue: newValue };
});
},
[transformationWrapper]
);
const unwrap = useCallback(
(wrapper: string) => {
transformationWrapper((value, selectionStart, selectionEnd) => {
let newValue = "";
newValue += value.slice(0, selectionStart - wrapper.length);
newValue += value.slice(selectionStart, selectionEnd);
newValue += value.slice(wrapper.length + selectionEnd);
return { prependLength: -wrapper.length, transformedValue: newValue };
});
},
[transformationWrapper]
);
const toggleWrap = useCallback(
(wrapper: string, properties?: Record<string, string>, addInnerNewLines?: boolean) => {
if (textAreaRef.current) {
const { value, selectionStart, selectionEnd } = textAreaRef.current;
if (
value.slice(selectionStart - wrapper.length, selectionStart) === wrapper &&
value.slice(selectionEnd, selectionEnd + wrapper.length) === wrapper
) {
unwrap(wrapper);
} else {
wrap(wrapper, properties, addInnerNewLines);
}
}
},
[unwrap, wrap]
);
const preline = useCallback(
(prepend: string) => {
transformationWrapper((value, selectionStart) => {
const lastNewLine = value.slice(0, selectionStart).lastIndexOf("\n") + 1;
let newValue = "";
newValue += value.slice(0, lastNewLine);
newValue += prepend;
newValue += value.slice(lastNewLine);
return { prependLength: prepend.length, transformedValue: newValue };
});
},
[transformationWrapper]
);
const insert = useCallback(
(prepend: string) => {
transformationWrapper((value, selectionStart) => {
let newValue = "";
newValue += value.slice(0, selectionStart);
newValue += prepend;
newValue += value.slice(selectionStart);
return { prependLength: prepend.length, transformedValue: newValue };
});
},
[transformationWrapper]
);
const appendDoc = useCallback(
(append: string) => {
transformationWrapper((value) => {
const newValue = value + append;
return { prependLength: 0, transformedValue: newValue };
});
},
[transformationWrapper]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<Popup isVisible={converterOpened} onCloseRequest={() => setConverterOpened(false)}>
<div className="text-center">
<h2 className="mt-4">Convert HTML to markdown</h2>
<p>
Copy and paste any HTML content (content from web pages) here.
<br />
The text will immediatly be converted to valid Markdown.
<br />
You can then copy the converted text and paste it anywhere you want in the editor
</p>
</div>
<textarea
readOnly
title="Ouput textarea"
onPaste={(event) => {
const turndownService = new TurndownService({
headingStyle: "atx",
codeBlockStyle: "fenced",
bulletListMarker: "-",
emDelimiter: "_",
strongDelimiter: "**",
});
let paste = event.clipboardData.getData("text/html");
paste = paste.replace(/<!--.*?-->/u, "");
paste = turndownService.turndown(paste);
paste = paste.replace(/<!--.*?-->/u, "");
const target = event.target as HTMLTextAreaElement;
target.value = paste;
target.select();
event.preventDefault();
}}
className="h-[50vh] w-[50vw]"
/>
</Popup>
<div className="mb-4 flex flex-row gap-2">
<ToolTip
content={
<div className="grid gap-2">
<h3 className="text-lg">Headers</h3>
<Button onClick={() => preline("# ")} text={"H1"} />
<Button onClick={() => preline("## ")} text={"H2"} />
<Button onClick={() => preline("### ")} text={"H3"} />
<Button onClick={() => preline("#### ")} text={"H4"} />
<Button onClick={() => preline("##### ")} text={"H5"} />
<Button onClick={() => preline("###### ")} text={"H6"} />
</div>
}>
<Button icon={Icon.Title} />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Bold</h3>}>
<Button onClick={() => toggleWrap("**")} icon={Icon.FormatBold} />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Toggle Italic</h3>}>
<Button onClick={() => toggleWrap("_")} icon={Icon.FormatItalic} />
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Toggle Inline Code</h3>
<p>
Makes the text monospace (like text from a computer terminal). Usually used for
stylistic purposes in transcripts.
</p>
</>
}>
<Button onClick={() => toggleWrap("`")} icon={Icon.Code} />
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Insert footnote</h3>
<p>When inserted &ldquo;x&rdquo;</p>
</>
}>
<Button
onClick={() => {
insert("[^x]");
appendDoc("\n\n[^x]: This is a footnote.");
}}
icon={Icon.Superscript}
/>
</ToolTip>
<ToolTip
placement="bottom"
content={
<>
<h3 className="text-lg">Transcripts</h3>
<p>
Use this to create dialogues and transcripts. Start by adding a container, then
add transcript speech line within.
</p>
<div className="grid gap-2">
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Transcript container</h3>
</>
}>
<Button onClick={() => wrap("Transcript", {}, true)} icon={Icon.AddBox} />
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Transcript speech line</h3>
<p>
Use to add a dialogue/transcript line. Change the <kbd>name</kbd> property
to chang the name of the speaker
</p>
</>
}>
<Button
onClick={() => wrap("Line", { name: "speaker" })}
icon={Icon.RecordVoiceOver}
/>
</ToolTip>
</div>
</>
}>
<Button icon={Icon.RecordVoiceOver} />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Inset box</h3>}>
<Button onClick={() => wrap("InsetBox", {}, true)} icon={Icon.CheckBoxOutlineBlank} />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Scene break</h3>}>
<Button onClick={() => insert("\n* * *\n")} icon={Icon.MoreHoriz} />
</ToolTip>
<ToolTip
content={
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-lg">Links</h3>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">External Link</h3>
<p className="text-xs">Provides a link to another webpage / website</p>
</>
}>
<Button
onClick={() => insert("[Link name](https://domain.com)")}
icon={Icon.Link}
text={"External"}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Intralink</h3>
<p className="text-xs">
Interlinks are used to add links to a header within the same document
</p>
</>
}>
<Button
onClick={() => wrap("IntraLink", {})}
icon={Icon.Link}
text={"Internal"}
/>
</ToolTip>
<ToolTip
placement="right"
content={
<>
<h3 className="text-lg">Intralink (with target)</h3>{" "}
<p className="text-xs">
Use this one if you want the intralink text to be different from the target
header&rsquo;s name.
</p>
</>
}>
<Button
onClick={() => wrap("IntraLink", { target: "target" })}
icon={Icon.Link}
text="Internal (w/ target)"
/>
</ToolTip>
</div>
}>
<Button icon={Icon.Link} />
</ToolTip>
<ToolTip
placement="bottom"
content={<h3 className="text-lg">Player&rsquo;s name placeholder</h3>}>
<Button onClick={() => insert("@player")} icon={Icon.Person} />
</ToolTip>
<ToolTip placement="bottom" content={<h3 className="text-lg">Open HTML Converter</h3>}>
<Button
onClick={() => {
setConverterOpened(true);
}}
icon={Icon.Html}
/>
</ToolTip>
</div>
<div className="grid grid-cols-2 gap-8">
<div>
<h2>Editor</h2>
<textarea
ref={textAreaRef}
onInput={(event) => {
const textarea = event.target as HTMLTextAreaElement;
handleInput(textarea.value);
}}
className="h-[70vh] w-full rounded-xl bg-mid !bg-opacity-40 p-8
font-mono text-black outline-none"
value={markdown}
title="Input textarea"
/>
</div>
<div>
<h2>Preview</h2>
<div className="h-[70vh] overflow-scroll rounded-xl bg-mid bg-opacity-40 p-8">
<Markdawn className="w-full" text={markdown} />
</div>
</div>
</div>
<div className="mt-8">
<TableOfContents text={markdown} />
</div>
</ContentPanel>
),
[appendDoc, converterOpened, handleInput, insert, markdown, preline, toggleWrap, wrap]
);
return <AppLayout contentPanel={contentPanel} {...props} />;
};
export default Editor;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Markdawn Editor"),
};
return {
props: props,
};
};

View File

@ -1,961 +0,0 @@
/* eslint-disable id-denylist */
import { GetStaticProps } from "next";
import { ReactNode, useState } from "react";
import Slider from "rc-slider";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { getLangui } from "graphql/fetchLocalData";
import { getOpenGraph } from "helpers/openGraph";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { Button } from "components/Inputs/Button";
import { Icon } from "components/Ico";
import { cJoin } from "helpers/className";
import { HorizontalLine } from "components/HorizontalLine";
import { Switch } from "components/Inputs/Switch";
import { TextInput } from "components/Inputs/TextInput";
import { Select } from "components/Inputs/Select";
import { WithLabel } from "components/Inputs/WithLabel";
import { NavOption } from "components/PanelComponents/NavOption";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { PreviewCard } from "components/PreviewCard";
import { PreviewLine } from "components/PreviewLine";
import { ChroniclePreview } from "components/Chronicles/ChroniclePreview";
import { PreviewFolder } from "components/Contents/PreviewFolder";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const DesignSystem = (props: Props): JSX.Element => {
const [switchState, setSwitchState] = useState(false);
const [selectState, setSelectState] = useState(0);
const [sliderState, setSliderState] = useState(5);
const [textInputState, setTextInputState] = useState("");
const [textAreaState, setTextAreaState] = useState("");
const [buttonGroupState, setButtonGroupState] = useState(0);
const contentPanel = (
<ContentPanel
className="grid place-items-center text-center"
width={ContentPanelWidthSizes.Full}>
<h1 className="mb-8 text-4xl">Design System</h1>
<h2 className="mb-4 text-3xl">Colors</h2>
<WhiteSection className="grid grid-cols-[repeat(7,auto)] place-items-center gap-4">
<p />
<p>Highlight</p>
<p>Light</p>
<p>Mid</p>
<p>Dark</p>
<p>Shade</p>
<p>Black</p>
<p>Light theme</p>
<ColorSquare className="bg-highlight" />
<ColorSquare className="bg-light" />
<ColorSquare className="bg-mid" />
<ColorSquare className="bg-dark" />
<ColorSquare className="bg-shade" />
<ColorSquare className="bg-black" />
<p>Dark theme</p>
<ColorSquare className="bg-highlight set-theme-dark" />
<ColorSquare className="bg-light set-theme-dark" />
<ColorSquare className="bg-mid set-theme-dark" />
<ColorSquare className="bg-dark set-theme-dark" />
<ColorSquare className="bg-shade set-theme-dark" />
<ColorSquare className="bg-black set-theme-dark" />
</WhiteSection>
<h2 className="mb-4 text-3xl">Fonts</h2>
<WhiteSection className="grid grid-cols-[repeat(5,auto)] place-items-start gap-y-2 gap-x-12">
<p />
<p className="font-headers text-xl text-black/50">Vollkorn</p>
<p className="font-body text-xl text-black/50">Zen Maru Gothic</p>
<p className="font-mono text-xl text-black/50">Share Tech Mono</p>
<p className="font-openDyslexic text-xl text-black/50">Open Dyslexic</p>
<p className="text-3xl text-black/30">3XL</p>
<p className="font-headers text-3xl">Header H3XL</p>
<p />
<p />
<p className="font-openDyslexic text-3xl">Dyslexia D3XL</p>
<p className="text-2xl text-black/30">2XL</p>
<p className="font-headers text-2xl">Header H2XL</p>
<p />
<p />
<p className="font-openDyslexic text-2xl">Dyslexia D2XL</p>
<p className="text-xl text-black/30">XL</p>
<p className="font-headers text-xl">Header HXL</p>
<p className="font-body text-xl">Body BXL</p>
<p className="font-mono text-xl">Mono MXL</p>
<p className="font-openDyslexic text-xl">Dyslexia DXL</p>
<p className="text-lg text-black/30">LG</p>
<p className="font-headers text-lg">Header HLG</p>
<p className="font-body text-lg">Body BLG</p>
<p className="font-mono text-lg">Mono MLG</p>
<p className="font-openDyslexic text-lg">Dyslexia DLG</p>
<p className="text-base text-black/30">B</p>
<p />
<p className="font-body text-base">Body BB</p>
<p className="font-mono text-base">Mono MB</p>
<p className="font-openDyslexic text-base">Dyslexia DB</p>
<p className="text-sm text-black/30">SM</p>
<p />
<p className="font-body text-sm">Body BSM</p>
<p />
<p className="font-openDyslexic text-sm">Dyslexia DSM</p>
<p className="text-xs text-black/30">XS</p>
<p />
<p />
<p />
<p className="font-openDyslexic text-xs">Dyslexia DXS</p>
</WhiteSection>
<h2 className="mb-4 text-3xl">Elevations</h2>
<TwoThemedSection
className="grid grid-cols-[repeat(7,auto)] place-content-center gap-4
text-left">
<ShadowSquare className="bg-light shadow-inner shadow-shade" text="IN" />
<ShadowSquare className="bg-light shadow-inner-sm shadow-shade" text="IN/SM" />
<ShadowSquare className="bg-light shadow-sm shadow-shade" text="SM" />
<ShadowSquare className="bg-light shadow-md shadow-shade" text="MD" />
<ShadowSquare className="bg-light shadow-lg shadow-shade" text="LG" />
<ShadowSquare className="bg-light shadow-xl shadow-shade" text="XL" />
<ShadowSquare className="bg-light shadow-2xl shadow-shade" text="2XL" />
<p className="mt-6">Drop shadow</p>
<p />
<ShadowSquare className="bg-light drop-shadow-sm shadow-shade" text="SM" />
<ShadowSquare className="bg-light drop-shadow-md shadow-shade" text="MD" />
<ShadowSquare className="bg-light drop-shadow-lg shadow-shade" text="LG" />
<ShadowSquare className="bg-light drop-shadow-xl shadow-shade" text="XL" />
<ShadowSquare className="bg-light drop-shadow-2xl shadow-shade" text="2XL" />
<p className="mt-6">Black</p>
<p />
<ShadowSquare className="bg-black text-light shadow-sm shadow-black" text="SM" />
<ShadowSquare className="bg-black text-light shadow-md shadow-black" text="MD" />
<ShadowSquare className="bg-black text-light shadow-lg shadow-black" text="LG" />
<ShadowSquare className="bg-black text-light shadow-xl shadow-black" text="XL" />
<ShadowSquare className="bg-black text-light shadow-2xl shadow-black" text="2XL" />
<p className="mt-6">
Drop shadow
<br />
black
</p>
<p />
<ShadowSquare className="bg-black text-light drop-shadow-sm shadow-black" text="SM" />
<ShadowSquare className="bg-black text-light drop-shadow-md shadow-black" text="MD" />
<ShadowSquare className="bg-black text-light drop-shadow-lg shadow-black" text="LG" />
<ShadowSquare className="bg-black text-light drop-shadow-xl shadow-black" text="XL" />
<ShadowSquare className="bg-black text-light drop-shadow-2xl shadow-black" text="2XL" />
</TwoThemedSection>
<h2 className="mb-4 text-3xl">Buttons</h2>
<TwoThemedSection className="grid gap-4">
<h3 className="text-xl">Normal sized</h3>
<div className="grid grid-cols-[repeat(4,auto)] place-content-center gap-4">
<p />
<p>Icon</p>
<p>Text</p>
<p>Icon + Text</p>
<p className="self-center justify-self-start">Normal</p>
<Button icon={Icon.Check} />
<Button text="Label" />
<Button icon={Icon.NavigateBefore} text="Label" />
<p className="self-center justify-self-start">Active</p>
<Button icon={Icon.Camera} active />
<Button text="Label" active />
<Button icon={Icon.NavigateBefore} text="Label" active />
<p className="self-center justify-self-start">Disabled</p>
<Button icon={Icon.Air} disabled />
<Button text="Label" disabled />
<Button icon={Icon.NavigateBefore} text="Label" disabled />
<p className="self-center justify-self-start">Badge</p>
<Button icon={Icon.Snooze} badgeNumber={5} />
<Button text="Label" badgeNumber={12} />
<Button icon={Icon.NavigateBefore} text="Label" badgeNumber={201} />
</div>
<HorizontalLine />
<h3 className="text-xl">Small sized</h3>
<div className="grid grid-cols-[repeat(4,auto)] place-content-center gap-4">
<p className="self-center justify-self-start">Normal</p>
<Button icon={Icon.Check} size={"small"} />
<Button text="Label" size={"small"} />
<Button icon={Icon.NavigateBefore} text="Label" size={"small"} />
<p className="self-center justify-self-start">Active</p>
<Button icon={Icon.Camera} active size={"small"} />
<Button text="Label" active size={"small"} />
<Button icon={Icon.NavigateBefore} text="Label" active size={"small"} />
<p className="self-center justify-self-start">Disabled</p>
<Button icon={Icon.Air} disabled size={"small"} />
<Button text="Label" disabled size={"small"} />
<Button icon={Icon.NavigateBefore} text="Label" disabled size={"small"} />
<p className="self-center justify-self-start">Badge</p>
<Button icon={Icon.Snooze} badgeNumber={5} size={"small"} />
<Button text="Label" badgeNumber={12} size={"small"} />
<Button icon={Icon.NavigateBefore} text="Label" badgeNumber={201} size={"small"} />
</div>
<HorizontalLine />
<h3 className="text-xl">Groups</h3>
<div className="grid place-items-center gap-4">
<ButtonGroup buttonsProps={[{ icon: Icon.CallEnd }, { icon: Icon.ZoomInMap }]} />
<ButtonGroup
buttonsProps={[
{ icon: Icon.CarCrash },
{ icon: Icon.TimeToLeave },
{ icon: Icon.LeakAdd },
]}
/>
<ButtonGroup
buttonsProps={[
{ icon: Icon.CarCrash },
{ icon: Icon.TimeToLeave, text: "Label", active: true },
{ text: "Another Label" },
{ icon: Icon.Cable },
]}
/>
<ButtonGroup
buttonsProps={[
{
text: "Try me!",
active: buttonGroupState === 0,
onClick: () => setButtonGroupState(0),
},
{
icon: Icon.AdUnits,
text: "Label",
active: buttonGroupState === 1,
onClick: () => setButtonGroupState(1),
},
{
text: "Yet another label",
active: buttonGroupState === 2,
onClick: () => setButtonGroupState(2),
},
{
icon: Icon.Security,
active: buttonGroupState === 3,
onClick: () => setButtonGroupState(3),
},
]}
/>
</div>
</TwoThemedSection>
<h2 className="mb-4 text-3xl">Inputs</h2>
<TwoThemedSection className="grid place-content-center gap-4">
<h3 className="text-xl">Switches</h3>
<WithLabel label="Off">
<Switch value={false} onClick={() => null} />
</WithLabel>
<WithLabel label="On">
<Switch value={true} onClick={() => null} />
</WithLabel>
<WithLabel label="Disabled (Off)">
<Switch value={false} onClick={() => null} disabled />
</WithLabel>
<WithLabel label="Disabled (On)">
<Switch value={true} onClick={() => null} disabled />
</WithLabel>
<WithLabel label={`Try me! (${switchState ? "On" : "Off"})`}>
<Switch value={switchState} onClick={() => setSwitchState((current) => !current)} />
</WithLabel>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Selects</h3>
<WithLabel label="Empty">
<Select
value={-1}
options={["Option 1", "Option 2", "Option 3", "Option 4"]}
onChange={() => null}
/>
</WithLabel>
<WithLabel label="Filled">
<Select
value={0}
options={["Option 1", "Option 2", "Option 3", "Option 4"]}
onChange={() => null}
/>
</WithLabel>
<WithLabel label="Filled + allow empty">
<Select
value={0}
options={["Option 1", "Option 2", "Option 3", "Option 4"]}
onChange={() => null}
allowEmpty
/>
</WithLabel>
<WithLabel label="Disabled">
<Select
value={0}
options={["Option 1", "Option 2", "Option 3", "Option 4"]}
onChange={() => null}
allowEmpty
disabled
/>
</WithLabel>
<WithLabel label="Try me!">
<Select
value={selectState}
options={["Option 1", "Option 2", "Option 3", "Option 4"]}
onChange={(index) => setSelectState(index)}
allowEmpty
/>
</WithLabel>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Text inputs</h3>
<WithLabel label="Empty">
<TextInput value="" onChange={() => null} />
</WithLabel>
<WithLabel label="Placeholder">
<TextInput value="" placeholder="Placeholder..." onChange={() => null} />
</WithLabel>
<WithLabel label="Filled">
<TextInput value="Value" onChange={() => null} />
</WithLabel>
<WithLabel label="Disabled">
<TextInput value="Value" onChange={() => null} disabled />
</WithLabel>
<WithLabel label="Try me!">
<TextInput
value={textInputState}
onChange={setTextInputState}
placeholder={"Placeholder..."}
/>
</WithLabel>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Text area</h3>
<WithLabel label="Empty">
<textarea value="" name="test" title="aria" />
</WithLabel>
<WithLabel label="Placeholder">
<textarea value="" placeholder="Placeholder..." />
</WithLabel>
<WithLabel label="Filled">
<textarea
value="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio.
Et consequatur dolorem omnis cupiditate."
placeholder="Placeholder..."
/>
</WithLabel>
<WithLabel label="Not resizable">
<textarea
className="resize-none"
value="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio.
Et consequatur dolorem omnis cupiditate."
placeholder="Placeholder..."
/>
</WithLabel>
<WithLabel label="Disabled">
<textarea
value="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio.
Et consequatur dolorem omnis cupiditate."
placeholder="Placeholder..."
disabled
/>
</WithLabel>
<WithLabel label="Try me!">
<textarea
value={textAreaState}
onChange={(event) => setTextAreaState(event.target.value)}
placeholder="Placeholder..."
/>
</WithLabel>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Slider</h3>
<WithLabel label="Normal">
<Slider value={5} />
</WithLabel>
<WithLabel label="Disabled">
<Slider value={5} disabled />
</WithLabel>
<WithLabel label="Try me!">
<Slider
value={sliderState}
max={100}
onChange={(event) => {
let value = 0;
if (Array.isArray(event)) {
value = event[0];
} else {
value = event;
}
setSliderState(() => value);
}}
/>
</WithLabel>
</TwoThemedSection>
<h2 className="mb-4 text-3xl">Down-Pressables</h2>
<TwoThemedSection className="grid gap-4">
<h3 className="mb-2 text-xl">Navigation Options</h3>
<div className="grid grid-cols-[repeat(6,auto)] place-items-center gap-4">
<p />
<p>Title</p>
<p>
Title
<br />+ Icon
</p>
<p>
Title
<br />+ Subtitle
</p>
<p>
Title
<br />+ Subtitle
<br />+ Icon
</p>
<p>Reduced</p>
<p>Normal</p>
<NavOption title="Title" url="#" />
<NavOption icon={Icon.Home} title="Title" url="#" />
<NavOption title="Title" subtitle="This is a subtitle" url="#" />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.CalendarMonth}
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.AccountBalance}
reduced
/>
<p>Border</p>
<NavOption title="Title" url="#" border />
<NavOption icon={Icon.TravelExplore} title="Title" url="#" border />
<NavOption title="Title" subtitle="This is a subtitle" url="#" border />
<NavOption title="Title" subtitle="This is a subtitle" url="#" icon={Icon.Help} border />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.TableRestaurant}
border
reduced
/>
<p>Active</p>
<NavOption title="Title" url="#" active />
<NavOption icon={Icon.Hail} title="Title" url="#" active />
<NavOption title="Title" subtitle="This is a subtitle" url="#" active />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Grading}
active
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Timer}
active
reduced
/>
<p>
Active
<br />+ Border
</p>
<NavOption title="Title" url="#" active />
<NavOption icon={Icon.Upcoming} title="Title" url="#" active border />
<NavOption title="Title" subtitle="This is a subtitle" url="#" active border />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Gamepad}
active
border
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Scale}
active
border
reduced
/>
<p>Disabled</p>
<NavOption title="Title" url="#" disabled />
<NavOption icon={Icon.Lan} title="Title" url="#" disabled />
<NavOption title="Title" subtitle="This is a subtitle" url="#" disabled />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.AlignHorizontalRight}
disabled
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.YoutubeSearchedFor}
reduced
disabled
/>
<p>
Disabled
<br />+ Border
</p>
<NavOption title="Title" url="#" border disabled />
<NavOption icon={Icon.Sanitizer} title="Title" url="#" border disabled />
<NavOption title="Title" subtitle="This is a subtitle" url="#" border disabled />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Pages}
border
disabled
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Synagogue}
border
reduced
disabled
/>
<p>
Disabled
<br />+ Active
</p>
<NavOption title="Title" url="#" active disabled />
<NavOption icon={Icon.Stairs} title="Title" url="#" active disabled />
<NavOption title="Title" subtitle="This is a subtitle" url="#" active disabled />
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Park}
active
disabled
/>
<NavOption
title="Title"
subtitle="This is a subtitle"
url="#"
icon={Icon.Password}
active
reduced
disabled
/>
</div>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Chronology Previews</h3>
<div className="grid grid-cols-[repeat(5,auto)] place-items-center gap-4">
<p />
<p>Title</p>
<p>Year</p>
<p>
Year
<br />+ Month
</p>
<p>
Year
<br />+ Month
<br />+ Day
</p>
<p>Normal</p>
<ChroniclePreview date={{}} title="Title" url="#" />
<ChroniclePreview date={{ year: 1970 }} title="Title" url="#" />
<ChroniclePreview date={{ year: 1970, month: 1 }} title="Title" url="#" />
<ChroniclePreview date={{ year: 1970, month: 1, day: 1 }} title="Title" url="#" />
<p>Active</p>
<ChroniclePreview date={{}} title="Title" url="#" active />
<ChroniclePreview date={{ year: 1970 }} title="Title" url="#" active />
<ChroniclePreview date={{ year: 1970, month: 1 }} title="Title" url="#" active />
<ChroniclePreview date={{ year: 1970, month: 1, day: 1 }} title="Title" url="#" active />
<p>Disabled</p>
<ChroniclePreview date={{}} title="Title" url="#" disabled />
<ChroniclePreview date={{ year: 1970 }} title="Title" url="#" disabled />
<ChroniclePreview date={{ year: 1970, month: 1 }} title="Title" url="#" disabled />
<ChroniclePreview
date={{ year: 1970, month: 1, day: 1 }}
title="Title"
url="#"
disabled
/>
<p>
Disabled
<br />
Active
</p>
<ChroniclePreview date={{}} title="Title" url="#" active disabled />
<ChroniclePreview date={{ year: 1970 }} title="Title" url="#" active disabled />
<ChroniclePreview date={{ year: 1970, month: 1 }} title="Title" url="#" active disabled />
<ChroniclePreview
date={{ year: 1970, month: 1, day: 1 }}
title="Title"
url="#"
active
disabled
/>
</div>
</TwoThemedSection>
<h2 className="mb-4 text-3xl">Up-Pressables</h2>
<TwoThemedSection className="grid gap-4">
<h3 className="-mt-6 mb-2 text-xl">Preview Cards</h3>
<div className="grid grid-cols-[repeat(2,auto)] place-items-center gap-4">
<PreviewCard
title="This one only has a title"
subtitle="And a subtitle"
href="#"
keepInfoVisible
/>
<PreviewCard
title="This one only has a title/subtitle"
subtitle="And a long description"
description="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio."
href="#"
thumbnail={"/default_og.jpg"}
keepInfoVisible
/>
<PreviewCard
pre_title="Breaking News"
title="This one only displays info"
subtitle="When it's hovered"
href="#"
thumbnail={"/default_og.jpg"}
/>
<PreviewCard
title="This one also has metadata at the top"
subtitle="And a subtitle"
description="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio."
href="#"
thumbnail={"/default_og.jpg"}
metadata={{
price: {
amount: 5.23,
currency: { data: { attributes: { code: "USD", rate_to_usd: 1, symbol: "$" } } },
},
releaseDate: { year: 1970, month: 1, day: 1 },
views: 550669,
position: "Top",
}}
/>
<PreviewCard
title="This one also has metadata at the bottom"
subtitle="And the thumbnail aspect ratio is forced to be 4:3"
href="#"
thumbnail={"/default_og.jpg"}
keepInfoVisible
thumbnailAspectRatio="4/3"
thumbnailForceAspectRatio
metadata={{
price: {
amount: 5.23,
currency: { data: { attributes: { code: "USD", rate_to_usd: 1, symbol: "$" } } },
},
releaseDate: { year: 1970, month: 1, day: 1 },
views: 550669,
position: "Bottom",
}}
/>
<PreviewCard
pre_title="Wow, that's a lot"
title="This one pretty much has everything"
subtitle="No joke, this is a lot of stuff"
href="#"
thumbnail={"/default_og.jpg"}
keepInfoVisible
infoAppend={<Button text="Another custom component" />}
bottomChips={["Bottom chip 1", "Chip 2", "Chip 3", "Chip 4"]}
description="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio."
hoverlay={{ __typename: "Video", duration: 465 }}
topChips={[
"Top chip 1",
"Chip 2",
"Chip 3",
"Chip 4",
"When there are too many, it overflow",
]}
metadata={{
price: {
amount: 5.23,
currency: { data: { attributes: { code: "USD", rate_to_usd: 1, symbol: "$" } } },
},
releaseDate: { year: 1970, month: 1, day: 1 },
views: 550669,
position: "Bottom",
}}
/>
<PreviewCard
title="This one is disabled"
subtitle="And a subtitle"
href="#"
thumbnail={"/default_og.jpg"}
keepInfoVisible
metadata={{
price: {
amount: 5.23,
currency: { data: { attributes: { code: "USD", rate_to_usd: 1, symbol: "$" } } },
},
releaseDate: { year: 1970, month: 1, day: 1 },
views: 550669,
position: "Bottom",
}}
disabled
/>
<PreviewCard
pre_title="Wow, that's a lot"
title="This one pretty much has everything"
subtitle="And it's disabled"
href="#"
thumbnail={"/default_og.jpg"}
keepInfoVisible
infoAppend={<Button text="Another custom component" />}
bottomChips={["Bottom chip 1", "Chip 2", "Chip 3", "Chip 4"]}
description="Eveniet occaecati qui dicta explicabo dolor.
Ipsum quam dolorum dolores.
Neque dolor nihil neque tempora.
Mollitia voluptates iste qui et temporibus eum omnis.
Itaque atque architecto maiores qui et optio."
hoverlay={{ __typename: "Video", duration: 465 }}
topChips={[
"Top chip 1",
"Chip 2",
"Chip 3",
"Chip 4",
"When there are too many, it overflow",
]}
metadata={{
price: {
amount: 5.23,
currency: { data: { attributes: { code: "USD", rate_to_usd: 1, symbol: "$" } } },
},
releaseDate: { year: 1970, month: 1, day: 1 },
views: 550669,
position: "Bottom",
}}
disabled
/>
</div>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Preview Line</h3>
<div className="grid grid-cols-[repeat(2,auto)] place-items-center gap-4">
<PreviewLine
href="#"
pre_title="Breaking News"
title="Accord's Library is live"
subtitle="I know, big deal, this is subtitle"
/>
<PreviewLine
href="#"
pre_title="Breaking News"
title="Accord's Library is live"
subtitle="I know, big deal, this is subtitle"
thumbnail={"/default_og.jpg"}
/>
<PreviewLine
href="#"
pre_title="Breaking News"
title="Accord's Library is live"
subtitle="I know, big deal, this is subtitle"
thumbnail={"/default_og.jpg"}
topChips={["Top chip 1", "Chip 2", "Chip 3", "Chip 4"]}
/>
<PreviewLine
href="#"
pre_title="Breaking News"
title="This one has everything"
subtitle="I know, big deal, this is subtitle"
thumbnail={"/default_og.jpg"}
topChips={["Top chip 1", "Chip 2", "Chip 3", "Chip 4"]}
bottomChips={["Bottom chip 1", "Chip 2", "Chip 3", "Chip 4"]}
/>
<PreviewLine
href="#"
title="Just a title"
thumbnail={"/default_og.jpg"}
topChips={["Top chip 1", "Chip 2", "Chip 3", "Chip 4"]}
bottomChips={["Bottom chip 1", "Chip 2", "Chip 3", "Chip 4"]}
/>
<PreviewLine
href="#"
title="Disabled"
thumbnail={"/default_og.jpg"}
topChips={["Top chip 1", "Chip 2", "Chip 3", "Chip 4"]}
bottomChips={["Bottom chip 1", "Chip 2", "Chip 3", "Chip 4"]}
disabled
/>
</div>
<HorizontalLine />
<h3 className="-mt-6 mb-2 text-xl">Folder Card</h3>
<div className="grid grid-cols-[repeat(2,auto)] place-items-center gap-4">
<PreviewFolder href="#" title="Title" />
<PreviewFolder href="#" title="A longer title, I guess" />
<PreviewFolder href="#" title="Disabled" disabled />
<PreviewFolder href="#" title="Disabled, with a longer title" disabled />
</div>
</TwoThemedSection>
</ContentPanel>
);
return <AppLayout {...props} contentPanel={contentPanel} />;
};
export default DesignSystem;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Design System"),
};
return {
props: props,
};
};
/*
*
* PRIVATE COMPONENTS
*/
interface ThemedSectionProps {
className?: string;
children?: ReactNode;
}
const TwoThemedSection = ({ children, className }: ThemedSectionProps) => (
<div className="mb-12 grid grid-flow-col drop-shadow-lg shadow-shade">
<LightThemeSection className={cJoin("rounded-l-xl text-black", className)}>
{children}
</LightThemeSection>
<DarkThemeSection className={cJoin("rounded-r-xl text-black", className)}>
{children}
</DarkThemeSection>
</div>
);
const DarkThemeSection = ({ className, children }: ThemedSectionProps) => (
<div className={cJoin("bg-light py-10 px-14 set-theme-dark", className)}>{children}</div>
);
const LightThemeSection = ({ className, children }: ThemedSectionProps) => (
<div className={cJoin("bg-light py-10 px-14 set-theme-light", className)}>{children}</div>
);
const WhiteSection = ({ className, children }: ThemedSectionProps) => (
<div className="mb-12 rounded-xl bg-[white] py-10 px-14 drop-shadow-lg shadow-shade">
<div className={cJoin("text-black set-theme-light", className)}>{children}</div>
</div>
);
interface ColorSquareProps {
className?: string;
}
const ColorSquare = ({ className }: ColorSquareProps) => (
<div className={cJoin("h-24 w-24 rounded-lg shadow-inner-sm shadow-shade", className)} />
);
interface ShadowSquareProps {
className?: string;
text: string;
}
const ShadowSquare = ({ className, text }: ShadowSquareProps) => (
<div className={cJoin("mb-12 grid h-20 w-20 place-content-center rounded-lg", className)}>
{text}
</div>
);

View File

@ -1,558 +0,0 @@
import { GetStaticProps } from "next";
import { useCallback, useMemo, useRef, useState } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Button } from "components/Inputs/Button";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { ToolTip } from "components/ToolTip";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
/*
*
* CONSTANTS
*/
const SIZE_MULTIPLIER = 1000;
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const replaceSelection = (
text: string,
selectionStart: number,
selectionEnd: number,
newSelectedText: string
) => text.substring(0, selectionStart) + newSelectedText + text.substring(selectionEnd);
const swapChar = (char: string, swaps: string[]): string => {
for (let index = 0; index < swaps.length; index++) {
if (char === swaps[index]) {
return swaps[(index + 1) % swaps.length];
}
}
return char;
};
const Transcript = (props: Props): JSX.Element => {
const [text, setText] = useState("");
const [fontSize, setFontSize] = useState(1);
const [xOffset, setXOffset] = useState(0);
const [lineIndex, setLineIndex] = useState(0);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const updateDisplayedText = useCallback(() => {
if (textAreaRef.current) {
setText(textAreaRef.current.value);
}
}, []);
const updateLineIndex = useCallback(() => {
if (textAreaRef.current) {
const subText = textAreaRef.current.value.substring(0, textAreaRef.current.selectionStart);
setLineIndex(subText.split("\n").length - 1);
}
}, []);
const convertFullWidth = useCallback(() => {
if (textAreaRef.current) {
textAreaRef.current.value = textAreaRef.current.value
// Numbers
.replaceAll("0", "")
.replaceAll("1", "")
.replaceAll("2", "")
.replaceAll("3", "")
.replaceAll("4", "")
.replaceAll("5", "")
.replaceAll("6", "")
.replaceAll("7", "")
.replaceAll("8", "")
.replaceAll("9", "")
// Uppercase letters
.replaceAll("A", "")
.replaceAll("B", "")
.replaceAll("C", "")
.replaceAll("D", "")
.replaceAll("E", "")
.replaceAll("F", "")
.replaceAll("G", "")
.replaceAll("H", "")
.replaceAll("I", "")
.replaceAll("J", "")
.replaceAll("K", "")
.replaceAll("L", "")
.replaceAll("M", "")
.replaceAll("N", "")
.replaceAll("O", "")
.replaceAll("P", "")
.replaceAll("Q", "")
.replaceAll("R", "")
.replaceAll("S", "")
.replaceAll("T", "")
.replaceAll("U", "")
.replaceAll("V", "")
.replaceAll("W", "")
.replaceAll("X", "")
.replaceAll("Y", "")
.replaceAll("Z", "")
// Lowercase letters
.replaceAll("a", "")
.replaceAll("b", "")
.replaceAll("c", "")
.replaceAll("d", "")
.replaceAll("e", "")
.replaceAll("f", "")
.replaceAll("g", "")
.replaceAll("h", "")
.replaceAll("i", "")
.replaceAll("j", "")
.replaceAll("k", "")
.replaceAll("l", "")
.replaceAll("m", "")
.replaceAll("n", "")
.replaceAll("o", "")
.replaceAll("p", "")
.replaceAll("q", "")
.replaceAll("r", "")
.replaceAll("s", "")
.replaceAll("t", "")
.replaceAll("u", "")
.replaceAll("v", "")
.replaceAll("w", "")
.replaceAll("x", "")
.replaceAll("y", "")
.replaceAll("z", "")
// Others
.replaceAll(" ", " ")
.replaceAll(",", "")
.replaceAll(".", "")
.replaceAll(":", "")
.replaceAll(";", "")
.replaceAll("!", "")
.replaceAll("?", "")
.replaceAll('"', "")
.replaceAll("'", "")
.replaceAll("`", "")
.replaceAll("^", "")
.replaceAll("~", "")
.replaceAll("_", "_")
.replaceAll("&", "")
.replaceAll("@", "")
.replaceAll("#", "")
.replaceAll("%", "")
.replaceAll("+", "")
.replaceAll("-", "")
.replaceAll("*", "")
.replaceAll("=", "")
.replaceAll("<", "")
.replaceAll(">", "")
.replaceAll("(", "")
.replaceAll(")", "")
.replaceAll("[", "")
.replaceAll("]", "")
.replaceAll("{", "")
.replaceAll("}", "")
.replaceAll("|", "")
.replaceAll("$", "")
.replaceAll("£", "£")
.replaceAll("¢", "¢")
.replaceAll("₩", "₩")
.replaceAll("¥", "¥");
updateDisplayedText();
}
}, [updateDisplayedText]);
const convertPunctuation = useCallback(() => {
if (textAreaRef.current) {
textAreaRef.current.value = textAreaRef.current.value
.replaceAll("...", "⋯")
.replaceAll("…", "⋯")
.replaceAll(":::", "⋯⋯")
.replaceAll(".", "。")
.replaceAll(",", "、")
.replaceAll("?", "")
.replaceAll("!", "")
.replaceAll(":", "")
.replaceAll("~", "");
updateDisplayedText();
}
}, [updateDisplayedText]);
const toggleDakuten = useCallback(() => {
if (textAreaRef.current) {
const selectionStart = Math.min(
textAreaRef.current.selectionStart,
textAreaRef.current.selectionEnd
);
const selectionEnd = Math.max(
textAreaRef.current.selectionStart,
textAreaRef.current.selectionEnd
);
const selection = textAreaRef.current.value.substring(selectionStart, selectionEnd);
if (selection.length === 1) {
let newSelection = selection;
/*
* Hiragana
* a
*/
newSelection = swapChar(newSelection, ["か", "が"]);
newSelection = swapChar(newSelection, ["さ", "ざ"]);
newSelection = swapChar(newSelection, ["た", "だ"]);
newSelection = swapChar(newSelection, ["は", "ば", "ぱ"]);
// i
newSelection = swapChar(newSelection, ["き", "ぎ"]);
newSelection = swapChar(newSelection, ["し", "じ"]);
newSelection = swapChar(newSelection, ["ち", "ぢ"]);
newSelection = swapChar(newSelection, ["ひ", "び", "ぴ"]);
// u
newSelection = swapChar(newSelection, ["く", "ぐ"]);
newSelection = swapChar(newSelection, ["す", "ず"]);
newSelection = swapChar(newSelection, ["つ", "づ"]);
newSelection = swapChar(newSelection, ["ふ", "ぶ", "ぷ"]);
// e
newSelection = swapChar(newSelection, ["け", "げ"]);
newSelection = swapChar(newSelection, ["せ", "ぜ"]);
newSelection = swapChar(newSelection, ["て", "で"]);
newSelection = swapChar(newSelection, ["へ", "べ", "ぺ"]);
// o
newSelection = swapChar(newSelection, ["こ", "ご"]);
newSelection = swapChar(newSelection, ["そ", "ぞ"]);
newSelection = swapChar(newSelection, ["と", "ど"]);
newSelection = swapChar(newSelection, ["ほ", "ぼ", "ぽ"]);
// others
newSelection = swapChar(newSelection, ["う", "ゔ"]);
newSelection = swapChar(newSelection, ["ゝ", "ゞ"]);
/*
* Katakana
* a
*/
newSelection = swapChar(newSelection, ["カ", "ガ"]);
newSelection = swapChar(newSelection, ["サ", "ザ"]);
newSelection = swapChar(newSelection, ["タ", "ダ"]);
newSelection = swapChar(newSelection, ["ハ", "バ", "パ"]);
// i
newSelection = swapChar(newSelection, ["キ", "ギ"]);
newSelection = swapChar(newSelection, ["シ", "ジ"]);
newSelection = swapChar(newSelection, ["チ", "ヂ"]);
newSelection = swapChar(newSelection, ["ヒ", "ビ", "ピ"]);
// u
newSelection = swapChar(newSelection, ["ク", "グ"]);
newSelection = swapChar(newSelection, ["ス", "ズ"]);
newSelection = swapChar(newSelection, ["ツ", "ヅ"]);
newSelection = swapChar(newSelection, ["フ", "ブ", "プ"]);
// e
newSelection = swapChar(newSelection, ["ケ", "ゲ"]);
newSelection = swapChar(newSelection, ["セ", "ゼ"]);
newSelection = swapChar(newSelection, ["テ", "デ"]);
newSelection = swapChar(newSelection, ["ヘ", "ベ", "ペ"]);
// o
newSelection = swapChar(newSelection, ["コ", "ゴ"]);
newSelection = swapChar(newSelection, ["ソ", "ゾ"]);
newSelection = swapChar(newSelection, ["ト", "ド"]);
newSelection = swapChar(newSelection, ["ホ", "ボ", "ポ"]);
// others
newSelection = swapChar(newSelection, ["ゥ", "ヴ"]);
newSelection = swapChar(newSelection, ["ヽ", "ヾ"]);
if (newSelection !== selection) {
textAreaRef.current.value = replaceSelection(
textAreaRef.current.value,
selectionStart,
selectionEnd,
newSelection
);
textAreaRef.current.selectionStart = selectionStart;
textAreaRef.current.selectionEnd = selectionEnd;
textAreaRef.current.focus();
updateDisplayedText();
}
}
}
}, [updateDisplayedText]);
const toggleSmallForm = useCallback(() => {
if (textAreaRef.current) {
const selectionStart = Math.min(
textAreaRef.current.selectionStart,
textAreaRef.current.selectionEnd
);
const selectionEnd = Math.max(
textAreaRef.current.selectionStart,
textAreaRef.current.selectionEnd
);
const selection = textAreaRef.current.value.substring(selectionStart, selectionEnd);
if (selection.length === 1) {
let newSelection = selection;
// Hiragana
newSelection = swapChar(newSelection, ["あ", "ぁ"]);
newSelection = swapChar(newSelection, ["い", "ぃ"]);
newSelection = swapChar(newSelection, ["う", "ぅ"]);
newSelection = swapChar(newSelection, ["え", "ぇ"]);
newSelection = swapChar(newSelection, ["お", "ぉ"]);
newSelection = swapChar(newSelection, ["か", "ゕ"]);
newSelection = swapChar(newSelection, ["け", "ゖ"]);
newSelection = swapChar(newSelection, ["つ", "っ"]);
newSelection = swapChar(newSelection, ["や", "ゃ"]);
newSelection = swapChar(newSelection, ["ゆ", "ゅ"]);
newSelection = swapChar(newSelection, ["よ", "ょ"]);
newSelection = swapChar(newSelection, ["わ", "ゎ"]);
// Katakana
newSelection = swapChar(newSelection, ["ア", "ァ"]);
newSelection = swapChar(newSelection, ["イ", "ィ"]);
newSelection = swapChar(newSelection, ["ウ", "ゥ"]);
newSelection = swapChar(newSelection, ["エ", "ェ"]);
newSelection = swapChar(newSelection, ["オ", "ォ"]);
newSelection = swapChar(newSelection, ["ツ", "ッ"]);
newSelection = swapChar(newSelection, ["ヤ", "ャ"]);
newSelection = swapChar(newSelection, ["ユ", "ュ"]);
newSelection = swapChar(newSelection, ["ヨ", "ョ"]);
if (newSelection !== selection) {
textAreaRef.current.value = replaceSelection(
textAreaRef.current.value,
selectionStart,
selectionEnd,
newSelection
);
textAreaRef.current.selectionStart = selectionStart;
textAreaRef.current.selectionEnd = selectionEnd;
textAreaRef.current.focus();
updateDisplayedText();
}
}
}
}, [updateDisplayedText]);
const insert = useCallback(
(insertedText: string) => {
if (textAreaRef.current) {
const selectionEnd = Math.max(
textAreaRef.current.selectionStart,
textAreaRef.current.selectionEnd
);
textAreaRef.current.value = replaceSelection(
textAreaRef.current.value,
selectionEnd,
selectionEnd,
insertedText
);
textAreaRef.current.selectionStart = selectionEnd;
textAreaRef.current.selectionEnd = selectionEnd + insertedText.length;
textAreaRef.current.focus();
updateDisplayedText();
}
},
[updateDisplayedText]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full} className="overflow-hidden !pr-0 !pt-4">
<div className="grid grid-flow-col grid-cols-[1fr_5rem]">
<textarea
ref={textAreaRef}
onChange={updateDisplayedText}
onClick={updateLineIndex}
onKeyUp={updateLineIndex}
title="Input textarea"
className="whitespace-pre"
/>
<p
className="h-[80vh] whitespace-nowrap font-[initial] font-bold
[writing-mode:vertical-rl] [transform-origin:top_right]"
style={{
transform: `scale(${fontSize}) translateX(${fontSize * xOffset}px)`,
}}>
{text.split("\n")[lineIndex]}
</p>
</div>
<div className="flex flex-wrap place-items-center gap-4 pr-24">
<div className="grid place-items-center">
<p>Text offset: {xOffset}px</p>
<input
title="Font size multiplier"
type="range"
min="0"
max="100"
value={xOffset * 10}
onChange={(event) => setXOffset(parseInt(event.target.value, 10) / 10)}
/>
</div>
<div className="grid place-items-center">
<p>Font size: {fontSize}x</p>
<input
title="Font size multiplier"
type="range"
min="1000"
max="3000"
value={fontSize * SIZE_MULTIPLIER}
onChange={(event) => setFontSize(parseInt(event.target.value, 10) / SIZE_MULTIPLIER)}
/>
</div>
<ToolTip content="Automatically convert Western punctuations to Japanese ones.">
<Button text=". ⟹ 。" onClick={convertPunctuation} />
</ToolTip>
<ToolTip content="Swap a kana for one of its variant (different diacritics).">
<Button text="か ⟺ が" onClick={toggleDakuten} />
</ToolTip>
<ToolTip content="Toggle a kana's small form">
<Button text="つ ⟺ っ" onClick={toggleSmallForm} />
</ToolTip>
<ToolTip content="Convert standard characters to their full width variant.">
<Button text="123 ⟹ " onClick={convertFullWidth} />
</ToolTip>
<ToolTip
content={
<div className="grid gap-2">
<ButtonGroup
buttonsProps={[
{ text: "「", onClick: () => insert("「") },
{ text: "」", onClick: () => insert("」") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "『", onClick: () => insert("『") },
{ text: "』", onClick: () => insert("』") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "【", onClick: () => insert("【") },
{ text: "】", onClick: () => insert("】") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "〖", onClick: () => insert("〖") },
{ text: "〗", onClick: () => insert("〗") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "〝", onClick: () => insert("〝") },
{ text: "〟", onClick: () => insert("〟") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "", onClick: () => insert("") },
{ text: "", onClick: () => insert("") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "⦅", onClick: () => insert("⦅") },
{ text: "⦆", onClick: () => insert("⦆") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "〈", onClick: () => insert("〈") },
{ text: "〉", onClick: () => insert("〉") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "《", onClick: () => insert("《") },
{ text: "》", onClick: () => insert("》") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "", onClick: () => insert("") },
{ text: "", onClick: () => insert("") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "", onClick: () => insert("") },
{ text: "", onClick: () => insert("") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "", onClick: () => insert("") },
{ text: "", onClick: () => insert("") },
]}
/>
<ButtonGroup
buttonsProps={[
{ text: "〘", onClick: () => insert("〘") },
{ text: "〙", onClick: () => insert("〙") },
]}
/>
</div>
}>
<Button text={"Quotations"} />
</ToolTip>
<ToolTip
content={
<div className="grid gap-2">
<Button text={"。"} onClick={() => insert("。")} />
<Button text={""} onClick={() => insert("")} />
<Button text={""} onClick={() => insert("")} />
<Button text={"⋯"} onClick={() => insert("⋯")} />
<Button text={"※"} onClick={() => insert("※")} />
<Button text={"♪"} onClick={() => insert("♪")} />
<Button text={"・"} onClick={() => insert("・")} />
<Button text={""} onClick={() => insert("")} />
<Button text={'" "'} onClick={() => insert(" ")} />
</div>
}>
<Button text="Insert" />
</ToolTip>
</div>
</ContentPanel>
),
[
convertFullWidth,
convertPunctuation,
fontSize,
insert,
lineIndex,
text,
toggleDakuten,
toggleSmallForm,
updateDisplayedText,
updateLineIndex,
xOffset,
]
);
return <AppLayout contentPanel={contentPanel} {...props} contentPanelScroolbar={false} />;
};
export default Transcript;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, "Japanese Transcription Tool"),
};
return {
props: props,
};
};

View File

@ -1,62 +0,0 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getOpenGraph } from "helpers/openGraph";
import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
if (isTerminalMode) {
return (
<Terminal
parentPath="/"
childrenPaths={[
"library",
"contents",
"wiki",
"chronicles",
"news",
"gallery",
"archives",
"about-us",
]}
/>
);
}
return (
<PostPage
{...otherProps}
prependBody={
<div className="grid w-full place-content-center place-items-center gap-5 text-center">
<div
className="aspect-square w-32 bg-black [mask:url('/icons/accords.svg')]
[mask-size:contain] [mask-repeat:no-repeat] [mask-position:center]"
/>
<h1 className="mb-0 text-5xl">Accord&rsquo;s Library</h1>
<h2 className="-mt-5 text-xl">Discover Analyze Translate Archive</h2>
</div>
}
displayTitle={false}
openGraph={getOpenGraph(langui)}
displayLanguageSwitcher
/>
);
};
export default Home;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps = getPostStaticProps("home");

View File

@ -1,806 +0,0 @@
import { Fragment, useCallback, useMemo } from "react";
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } 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/Containers/InsetBox";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { NavOption } from "components/PanelComponents/NavOption";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import {
Enum_Componentmetadatabooks_Binding_Type,
Enum_Componentmetadatabooks_Page_Order,
GetLibraryItemQuery,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import {
prettyDate,
prettyInlineTitle,
prettyItemSubType,
prettyItemType,
prettyPrice,
prettySlug,
prettyURL,
} from "helpers/formatters";
import { ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers";
import {
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
sortRangedContent,
} from "helpers/others";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel";
import { Ico, Icon } from "components/Ico";
import { cJoin, cIf } from "helpers/className";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { getOpenGraph } from "helpers/openGraph";
import { getDescription } from "helpers/description";
import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const intersectionIds = ["summary", "gallery", "details", "subitems", "contents"];
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
item: NonNullable<NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["attributes"]>;
itemId: NonNullable<GetLibraryItemQuery["libraryItems"]>["data"][number]["id"];
}
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
const currency = useAtomGetter(atoms.settings.currency);
const langui = useAtomGetter(atoms.localData.langui);
const currencies = useAtomGetter(atoms.localData.currencies);
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm);
const hoverable = useDeviceSupportsHover();
const router = useRouter();
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(false);
const { showLightBox } = useAtomGetter(atoms.lightBox);
useScrollTopOnChange(Ids.ContentPanel, [item]);
const currentIntersection = useIntersectionList(intersectionIds);
const isVariantSet = useMemo(
() =>
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
item.metadata[0].subtype?.data?.attributes?.slug === "variant-set",
[item.metadata]
);
const displayOpenScans = useMemo(
() =>
item.contents?.data.some(
(content) => content.attributes?.scan_set && content.attributes.scan_set.length > 0
),
[item.contents?.data]
);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton href="/library/" title={langui.library} displayOnlyOn="3ColumnsLayout" />
<HorizontalLine />
<div className="grid gap-4">
<NavOption
title={langui.summary}
url={`#${intersectionIds[0]}`}
border
active={currentIntersection === 0}
/>
{item.gallery && item.gallery.data.length > 0 && (
<NavOption
title={langui.gallery}
url={`#${intersectionIds[1]}`}
border
active={currentIntersection === 1}
/>
)}
<NavOption
title={langui.details}
url={`#${intersectionIds[2]}`}
border
active={currentIntersection === 2}
/>
{item.subitems && item.subitems.data.length > 0 && (
<NavOption
title={isVariantSet ? langui.variants : langui.subitems}
url={`#${intersectionIds[3]}`}
border
active={currentIntersection === 3}
/>
)}
{item.contents && item.contents.data.length > 0 && (
<NavOption
title={langui.contents}
url={`#${intersectionIds[4]}`}
border
active={currentIntersection === 4}
/>
)}
</div>
</SubPanel>
),
[currentIntersection, isVariantSet, item.contents, item.gallery, item.subitems, langui]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<ReturnButton
href="/library/"
title={langui.library}
displayOnlyOn="1ColumnLayout"
className="mb-10"
/>
<div className="grid place-items-center gap-12">
<div
className={cJoin(
"relative h-[50vh] w-full cursor-pointer drop-shadow-xl shadow-shade",
cIf(isContentPanelAtLeast3xl, "mb-16", "h-[60vh]")
)}>
{item.thumbnail?.data?.attributes ? (
<Img
src={item.thumbnail.data.attributes}
quality={ImageQuality.Large}
className="h-full w-full object-contain"
onClick={() => {
showLightBox([item.thumbnail?.data?.attributes]);
}}
/>
) : (
<div className="aspect-[21/29.7] w-full rounded-xl bg-light" />
)}
</div>
<InsetBox id={intersectionIds[0]} className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
{item.subitem_of?.data[0]?.attributes && (
<div className="grid place-items-center">
<p>{langui.subitem_of}</p>
<Button
href={`/library/${item.subitem_of.data[0].attributes.slug}`}
text={prettyInlineTitle(
"",
item.subitem_of.data[0].attributes.title,
item.subitem_of.data[0].attributes.subtitle
)}
/>
</div>
)}
<div className="grid place-items-center text-center">
<h1 className="text-3xl">{item.title}</h1>
{isDefinedAndNotEmpty(item.subtitle) && (
<h2 className="text-2xl">{item.subtitle}</h2>
)}
</div>
{!isUntangibleGroupItem(item.metadata?.[0]) && isDefinedAndNotEmpty(itemId) && (
<PreviewCardCTAs id={itemId} expand />
)}
{item.descriptions?.[0] && (
<p className="text-justify">{item.descriptions[0].description}</p>
)}
{!(
item.metadata &&
item.metadata[0]?.__typename === "ComponentMetadataGroup" &&
(item.metadata[0].subtype?.data?.attributes?.slug === "variant-set" ||
item.metadata[0].subtype?.data?.attributes?.slug === "relation-set")
) && (
<>
{item.urls?.length ? (
<div className="flex flex-row place-items-center gap-3">
<p>{langui.available_at}</p>
{filterHasAttributes(item.urls, ["url"] as const).map((url, index) => (
<Fragment key={index}>
<Button href={url.url} text={prettyURL(url.url)} alwaysNewTab />
</Fragment>
))}
</div>
) : (
<p>{langui.item_not_available}</p>
)}
</>
)}
</div>
</InsetBox>
{item.gallery && item.gallery.data.length > 0 && (
<div id={intersectionIds[1]} className="grid w-full place-items-center gap-8">
<h2 className="text-2xl">{langui.gallery}</h2>
<div
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
gap-8">
{filterHasAttributes(item.gallery.data, ["id", "attributes"] as const).map(
(galleryItem, index) => (
<Fragment key={galleryItem.id}>
<div
className="relative aspect-square cursor-pointer
transition-transform hover:scale-102"
onClick={() => {
showLightBox(
filterHasAttributes(item.gallery?.data, ["attributes"] as const).map(
(image) => image.attributes
),
index
);
}}>
<Img
className="h-full w-full rounded-lg bg-light object-cover shadow-md
shadow-shade/30 transition-shadow hover:shadow-lg hover:shadow-shade/50"
src={galleryItem.attributes}
/>
</div>
</Fragment>
)
)}
</div>
</div>
)}
<InsetBox id={intersectionIds[2]} className="grid place-items-center">
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-10">
<h2 className="text-center text-2xl">{langui.details}</h2>
<div
className={cJoin(
"grid place-items-center gap-y-8",
cIf(isContentPanelAtLeast3xl, "grid-flow-col place-content-between")
)}>
{item.metadata?.[0] && (
<div className="grid place-content-start place-items-center">
<h3 className="text-xl">{langui.type}</h3>
<div className="grid grid-flow-col gap-1">
<Chip text={prettyItemType(item.metadata[0], langui)} />
{""}
<Chip text={prettyItemSubType(item.metadata[0])} />
</div>
</div>
)}
{item.release_date && (
<div className="grid place-content-start place-items-center">
<h3 className="text-xl">{langui.release_date}</h3>
<p>{prettyDate(item.release_date, router.locale)}</p>
</div>
)}
{item.price && (
<div className="grid place-content-start place-items-center text-center">
<h3 className="text-xl">{langui.price}</h3>
<p>
{prettyPrice(
item.price,
currencies,
item.price.currency?.data?.attributes?.code
)}
</p>
{item.price.currency?.data?.attributes?.code !== currency && (
<p>
{prettyPrice(item.price, currencies, currency)} <br />(
{langui.calculated?.toLowerCase()})
</p>
)}
</div>
)}
</div>
{item.categories && item.categories.data.length > 0 && (
<div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(item.categories.data, ["attributes"] as const).map(
(category) => (
<Chip key={category.id} text={category.attributes.name} />
)
)}
</div>
</div>
)}
{item.size && (
<div
className={cJoin(
"grid gap-4",
cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}>
<h3 className="text-xl">{langui.size}</h3>
<div
className={cJoin(
"grid w-full",
cIf(
isContentPanelAtLeastSm,
"grid-flow-col place-content-between",
"grid-flow-row place-content-center gap-8"
)
)}>
<div
className={cJoin(
"grid gap-x-4",
cIf(
isContentPanelAtLeast3xl,
"grid-flow-col place-items-start",
"place-items-center"
)
)}>
<p className="font-bold">{langui.width}:</p>
<div>
<p>{item.size.width} mm</p>
<p>{convertMmToInch(item.size.width)} in</p>
</div>
</div>
<div
className={cJoin(
"grid gap-x-4",
cIf(
isContentPanelAtLeast3xl,
"grid-flow-col place-items-start",
"place-items-center"
)
)}>
<p className="font-bold">{langui.height}:</p>
<div>
<p>{item.size.height} mm</p>
<p>{convertMmToInch(item.size.height)} in</p>
</div>
</div>
{isDefined(item.size.thickness) && (
<div
className={cJoin(
"grid gap-x-4",
cIf(
isContentPanelAtLeast3xl,
"grid-flow-col place-items-start",
"place-items-center"
)
)}>
<p className="font-bold">{langui.thickness}:</p>
<div>
<p>{item.size.thickness} mm</p>
<p>{convertMmToInch(item.size.thickness)} in</p>
</div>
</div>
)}
</div>
</div>
)}
{item.metadata?.[0]?.__typename !== "ComponentMetadataGroup" &&
item.metadata?.[0]?.__typename !== "ComponentMetadataOther" && (
<div
className={cJoin(
"grid gap-4",
cIf(!isContentPanelAtLeast3xl, "place-items-center")
)}>
<h3 className="text-xl">{langui.type_information}</h3>
<div className="flex flex-wrap place-content-between gap-x-8">
{item.metadata?.[0]?.__typename === "ComponentMetadataBooks" && (
<>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.pages}:</p>
<p>{item.metadata[0].page_count}</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.binding}:</p>
<p>
{item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Paperback
? langui.paperback
: item.metadata[0].binding_type ===
Enum_Componentmetadatabooks_Binding_Type.Hardcover
? langui.hardcover
: ""}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.page_order}:</p>
<p>
{item.metadata[0].page_order ===
Enum_Componentmetadatabooks_Page_Order.LeftToRight
? langui.left_to_right
: langui.right_to_left}
</p>
</div>
<div className="flex flex-row place-content-start gap-4">
<p className="font-bold">{langui.languages}:</p>
{item.metadata[0]?.languages?.data.map((lang) => (
<p key={lang.attributes?.code}>{lang.attributes?.name}</p>
))}
</div>
</>
)}
</div>
</div>
)}
</div>
</InsetBox>
{item.subitems && item.subitems.data.length > 0 && (
<div id={intersectionIds[3]} className="grid w-full place-items-center gap-8">
<h2 className="text-2xl">{isVariantSet ? langui.variants : langui.subitems}</h2>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
</WithLabel>
)}
<div
className="grid w-full grid-cols-[repeat(auto-fill,minmax(13rem,1fr))]
items-end gap-8">
{filterHasAttributes(item.subitems.data, ["id", "attributes"] as const).map(
(subitem) => (
<Fragment key={subitem.id}>
<PreviewCard
href={`/library/${subitem.attributes.slug}`}
title={subitem.attributes.title}
subtitle={subitem.attributes.subtitle}
thumbnail={subitem.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7"
thumbnailRounded={false}
keepInfoVisible={keepInfoVisible}
topChips={
subitem.attributes.metadata &&
subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata[0]
? [prettyItemSubType(subitem.attributes.metadata[0])]
: []
}
bottomChips={subitem.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
metadata={{
releaseDate: subitem.attributes.release_date,
price: subitem.attributes.price,
position: "Bottom",
}}
infoAppend={
!isUntangibleGroupItem(subitem.attributes.metadata?.[0]) && (
<PreviewCardCTAs id={subitem.id} />
)
}
/>
</Fragment>
)
)}
</div>
</div>
)}
{item.contents && item.contents.data.length > 0 && (
<div id={intersectionIds[4]} className="grid w-full place-items-center gap-8">
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
{displayOpenScans && (
<div className="grid grid-flow-col gap-4">
<Button href={`/library/${item.slug}/reader`} text={langui.view_scans} />
</div>
)}
<div className="max-w- grid w-full gap-4">
{filterHasAttributes(item.contents.data, ["attributes"] as const).map(
(rangedContent) => (
<ContentLine
content={
rangedContent.attributes.content?.data?.attributes
? {
translations: filterDefined(
rangedContent.attributes.content.data.attributes.translations
).map((translation) => ({
pre_title: translation.pre_title,
title: translation.title,
subtitle: translation.subtitle,
language: translation.language?.data?.attributes?.code,
})),
categories: filterHasAttributes(
rangedContent.attributes.content.data.attributes.categories?.data,
["attributes"]
).map((category) => category.attributes.short),
type:
rangedContent.attributes.content.data.attributes.type?.data
?.attributes?.titles?.[0]?.title ??
prettySlug(
rangedContent.attributes.content.data.attributes.type?.data
?.attributes?.slug
),
slug: rangedContent.attributes.content.data.attributes.slug,
}
: undefined
}
rangeStart={
rangedContent.attributes.range[0]?.__typename === "ComponentRangePageRange"
? `${rangedContent.attributes.range[0].starting_page}`
: ""
}
slug={rangedContent.attributes.slug}
parentSlug={item.slug}
key={rangedContent.id}
hasScanSet={
isDefined(rangedContent.attributes.scan_set) &&
rangedContent.attributes.scan_set.length > 0
}
condensed={!isContentPanelAtLeast3xl}
/>
)
)}
</div>
</div>
)}
</div>
</ContentPanel>
),
[
langui,
isContentPanelAtLeast3xl,
item.thumbnail?.data?.attributes,
item.subitem_of?.data,
item.title,
item.subtitle,
item.metadata,
item.descriptions,
item.urls,
item.gallery,
item.release_date,
item.price,
item.categories,
item.size,
item.subitems,
item.contents,
item.slug,
itemId,
router.locale,
currencies,
currency,
isContentPanelAtLeastSm,
isVariantSet,
hoverable,
toggleKeepInfoVisible,
keepInfoVisible,
displayOpenScans,
showLightBox,
]
);
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
};
export default LibrarySlug;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const item = await sdk.getLibraryItem({
slug: context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "",
language_code: context.locale ?? "en",
});
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };
sortRangedContent(item.libraryItems.data[0].attributes.contents);
const { title, thumbnail } = item.libraryItems.data[0].attributes;
const description = getDescription(
item.libraryItems.data[0].attributes.descriptions?.[0]?.description,
{
[langui.categories ?? "Categories"]: filterHasAttributes(
item.libraryItems.data[0].attributes.categories?.data,
["attributes.short"]
).map((category) => category.attributes.short),
[langui.type ?? "Type"]: item.libraryItems.data[0].attributes.metadata?.[0]
? [prettyItemSubType(item.libraryItems.data[0].attributes.metadata[0])]
: [],
[langui.release_date ?? "Release date"]: [
item.libraryItems.data[0].attributes.release_date
? prettyDate(item.libraryItems.data[0].attributes.release_date, context.locale)
: undefined,
],
}
);
const props: Props = {
item: item.libraryItems.data[0].attributes,
itemId: item.libraryItems.data[0].id,
openGraph: getOpenGraph(langui, title, description, thumbnail?.data?.attributes),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const libraryItems = await sdk.getLibraryItemsSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(libraryItems.libraryItems?.data, ["attributes"] as const).map((item) => {
context.locales?.map((local) =>
paths.push({ params: { slug: item.attributes.slug }, locale: local })
);
});
return {
paths,
fallback: "blocking",
};
};
/*
*
* PRIVATE COMPONENTS
*/
interface ContentLineProps {
content?: {
translations: {
pre_title: string | null | undefined;
title: string;
subtitle: string | null | undefined;
language: string | undefined;
}[];
categories?: string[];
type?: string;
slug: string;
};
rangeStart: string;
parentSlug: string;
slug: string;
hasScanSet: boolean;
condensed: boolean;
}
const ContentLine = ({
rangeStart,
content,
hasScanSet,
slug,
parentSlug,
condensed,
}: ContentLineProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const { value: isOpened, toggle: toggleOpened } = useBoolean(false);
const [selectedTranslation] = useSmartLanguage({
items: content?.translations ?? [],
languageExtractor: useCallback(
(item: NonNullable<ContentLineProps["content"]>["translations"][number]) => item.language,
[]
),
});
if (condensed) {
return (
<div className="my-4 grid gap-2">
<div className="flex gap-2">
{content?.type && <Chip text={content.type} />}
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
<p>{rangeStart}</p>
</div>
<h3 className="flex flex-wrap place-items-center gap-2">
{selectedTranslation
? prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)
: content
? prettySlug(content.slug, parentSlug)
: prettySlug(slug, parentSlug)}
</h3>
<div className="flex flex-row flex-wrap gap-1">
{content?.categories?.map((category, index) => (
<Chip key={index} text={category} />
))}
</div>
<div className="grid grid-cols-2 gap-3">
{hasScanSet || isDefined(content) ? (
<>
{hasScanSet && (
<Button
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
text={langui.view_scans}
/>
)}
{isDefined(content) && (
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
)}
</>
) : (
langui.content_is_not_available
)}
</div>
</div>
);
}
return (
<div
className={cJoin(
"grid gap-2 rounded-lg px-4",
cIf(isOpened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade")
)}>
<div className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center gap-4">
<a>
<h3 className="cursor-pointer" onClick={toggleOpened}>
{selectedTranslation
? prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)
: content
? prettySlug(content.slug, parentSlug)
: prettySlug(slug, parentSlug)}
</h3>
</a>
<div className="flex flex-row flex-wrap gap-1">
{content?.categories?.map((category, index) => (
<Chip key={index} text={category} />
))}
</div>
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30" />
<p>{rangeStart}</p>
{content?.type && <Chip className="justify-self-end" text={content.type} />}
</div>
<div
className={`grid-flow-col place-content-start place-items-center gap-2 ${
isOpened ? "grid" : "hidden"
}`}>
<Ico icon={Icon.SubdirectoryArrowRight} className="text-dark" />
{hasScanSet || isDefined(content) ? (
<>
{hasScanSet && (
<Button
href={`/library/${parentSlug}/reader?page=${rangeStart}`}
text={langui.view_scans}
/>
)}
{isDefined(content) && (
<Button href={`/contents/${content.slug}`} text={langui.open_content} />
)}
</>
) : (
langui.content_is_not_available
)}
</div>
</div>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -1,507 +0,0 @@
import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react";
import { useBoolean } from "usehooks-ts";
import naturalCompare from "string-natural-compare";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Select } from "components/Inputs/Select";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettyInlineTitle, prettyItemSubType } from "helpers/formatters";
import { LibraryItemUserStatus } from "types/types";
import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { PreviewCard } from "components/PreviewCard";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { convertPrice } from "helpers/numbers";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
import { cIf, cJoin } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
searchName: "",
showSubitems: false,
showPrimaryItems: true,
showSecondaryItems: false,
sortingMethod: 0,
groupingMethod: -1,
keepInfoVisible: false,
filterUserStatus: undefined,
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
}
const Library = ({ items, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const currencies = useAtomGetter(atoms.localData.currencies);
const { libraryItemUserStatus } = useLibraryItemUserStatus();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const {
value: showSubitems,
toggle: toggleShowSubitems,
setValue: setShowSubitems,
} = useBoolean(DEFAULT_FILTERS_STATE.showSubitems);
const {
value: showPrimaryItems,
toggle: toggleShowPrimaryItems,
setValue: setShowPrimaryItems,
} = useBoolean(DEFAULT_FILTERS_STATE.showPrimaryItems);
const {
value: showSecondaryItems,
toggle: toggleShowSecondaryItems,
setValue: setShowSecondaryItems,
} = useBoolean(DEFAULT_FILTERS_STATE.showSecondaryItems);
const {
value: keepInfoVisible,
toggle: toggleKeepInfoVisible,
setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const [sortingMethod, setSortingMethod] = useState<number>(DEFAULT_FILTERS_STATE.sortingMethod);
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod
);
const [filterUserStatus, setFilterUserStatus] = useState<LibraryItemUserStatus | undefined>(
DEFAULT_FILTERS_STATE.filterUserStatus
);
const filteringFunction = useCallback(
(item: SelectiveNonNullable<Props["items"][number], "attributes" | "id">) => {
if (!showSubitems && !item.attributes.root_item) return false;
if (showSubitems && isUntangibleGroupItem(item.attributes.metadata?.[0])) {
return false;
}
if (item.attributes.primary && !showPrimaryItems) return false;
if (!item.attributes.primary && !showSecondaryItems) return false;
if (isDefined(filterUserStatus) && item.id) {
if (isUntangibleGroupItem(item.attributes.metadata?.[0])) {
return false;
}
if (filterUserStatus === LibraryItemUserStatus.None) {
if (libraryItemUserStatus[item.id]) {
return false;
}
} else if (filterUserStatus !== libraryItemUserStatus[item.id]) {
return false;
}
}
return true;
},
[libraryItemUserStatus, filterUserStatus, showPrimaryItems, showSecondaryItems, showSubitems]
);
const sortingFunction = useCallback(
(
a: SelectiveNonNullable<Props["items"][number], "attributes" | "id">,
b: SelectiveNonNullable<Props["items"][number], "attributes" | "id">
) => {
switch (sortingMethod) {
case 0: {
const titleA = prettyInlineTitle("", a.attributes.title, a.attributes.subtitle);
const titleB = prettyInlineTitle("", b.attributes.title, b.attributes.subtitle);
return naturalCompare(titleA, titleB);
}
case 1: {
const priceA = a.attributes.price
? convertPrice(a.attributes.price, currencies[0])
: Infinity;
const priceB = b.attributes.price
? convertPrice(b.attributes.price, currencies[0])
: Infinity;
return priceA - priceB;
}
case 2: {
return compareDate(a.attributes.release_date, b.attributes.release_date);
}
default:
return 0;
}
},
[currencies, sortingMethod]
);
const groupingFunction = useCallback(
(item: SelectiveNonNullable<Props["items"][number], "attributes" | "id">): string[] => {
switch (groupingMethod) {
case 0: {
const categories = filterHasAttributes(item.attributes.categories?.data, [
"attributes",
] as const);
if (categories.length > 0) {
return categories.map((category) => category.attributes.name);
}
return [langui.no_category ?? "No category"];
}
case 1: {
if (item.attributes.metadata && item.attributes.metadata.length > 0) {
switch (item.attributes.metadata[0]?.__typename) {
case "ComponentMetadataAudio":
return [langui.audio ?? "Audio"];
case "ComponentMetadataGame":
return [langui.game ?? "Game"];
case "ComponentMetadataBooks":
return [langui.textual ?? "Textual"];
case "ComponentMetadataVideo":
return [langui.video ?? "Video"];
case "ComponentMetadataOther":
return [langui.other ?? "Other"];
case "ComponentMetadataGroup": {
switch (item.attributes.metadata[0]?.subitems_type?.data?.attributes?.slug) {
case "audio":
return [langui.audio ?? "Audio"];
case "video":
return [langui.video ?? "Video"];
case "game":
return [langui.game ?? "Game"];
case "textual":
return [langui.textual ?? "Textual"];
case "mixed":
return [langui.group ?? "Group"];
default: {
return [langui.no_type ?? "No type"];
}
}
}
default:
return [langui.no_type ?? "No type"];
}
} else {
return [langui.no_type ?? "No type"];
}
}
case 2: {
if (item.attributes.release_date?.year) {
return [item.attributes.release_date.year.toString()];
}
return [langui.no_year ?? "No year"];
}
default:
return [""];
}
},
[groupingMethod, langui]
);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.LibraryBooks}
title={langui.library}
description={langui.library_description}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={(name) => {
setSearchName(name);
if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Library", "Change search term");
} else {
sendAnalytics("Library", "Clear search term");
}
}}
/>
<WithLabel label={langui.group_by}>
<Select
className="w-full"
options={[
langui.category ?? "Category",
langui.type ?? "Type",
langui.release_year ?? "Year",
]}
value={groupingMethod}
onChange={(value) => {
setGroupingMethod(value);
sendAnalytics(
"Library",
`Change grouping method (${["none", "category", "type", "year"][value + 1]})`
);
}}
allowEmpty
/>
</WithLabel>
<WithLabel label={langui.order_by}>
<Select
className="w-full"
options={[
langui.name ?? "Name",
langui.price ?? "Price",
langui.release_date ?? "Release date",
]}
value={sortingMethod}
onChange={(value) => {
setSortingMethod(value);
sendAnalytics(
"Library",
`Change sorting method (${["name", "price", "release date"][value]})`
);
}}
/>
</WithLabel>
<WithLabel label={langui.show_subitems}>
<Switch
value={showSubitems}
onClick={() => {
toggleShowSubitems();
sendAnalytics("Library", `${showSubitems ? "Hide" : "Show"} subitems`);
}}
/>
</WithLabel>
<WithLabel label={langui.show_primary_items}>
<Switch
value={showPrimaryItems}
onClick={() => {
toggleShowPrimaryItems();
sendAnalytics("Library", `${showPrimaryItems ? "Hide" : "Show"} primary items`);
}}
/>
</WithLabel>
<WithLabel label={langui.show_secondary_items}>
<Switch
value={showSecondaryItems}
onClick={() => {
toggleShowSecondaryItems();
sendAnalytics("Library", `${showSecondaryItems ? "Hide" : "Show"} secondary items`);
}}
/>
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch
value={keepInfoVisible}
onClick={() => {
toggleKeepInfoVisible();
sendAnalytics("Library", `Always ${keepInfoVisible ? "hide" : "show"} info`);
}}
/>
</WithLabel>
)}
<ButtonGroup
className="mt-4"
buttonsProps={[
{
tooltip: langui.only_display_items_i_want,
icon: Icon.Favorite,
onClick: () => {
setFilterUserStatus(LibraryItemUserStatus.Want);
sendAnalytics("Library", "Set filter status (I want)");
},
active: filterUserStatus === LibraryItemUserStatus.Want,
},
{
tooltip: langui.only_display_items_i_have,
icon: Icon.BackHand,
onClick: () => {
setFilterUserStatus(LibraryItemUserStatus.Have);
sendAnalytics("Library", "Set filter status (I have)");
},
active: filterUserStatus === LibraryItemUserStatus.Have,
},
{
tooltip: langui.only_display_unmarked_items,
icon: Icon.RadioButtonUnchecked,
onClick: () => {
setFilterUserStatus(LibraryItemUserStatus.None);
sendAnalytics("Library", "Set filter status (unmarked)");
},
active: filterUserStatus === LibraryItemUserStatus.None,
},
{
tooltip: langui.only_display_unmarked_items,
text: langui.all,
onClick: () => {
setFilterUserStatus(undefined);
sendAnalytics("Library", "Set filter status (all)");
},
active: isUndefined(filterUserStatus),
},
]}
/>
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setShowSubitems(DEFAULT_FILTERS_STATE.showSubitems);
setShowPrimaryItems(DEFAULT_FILTERS_STATE.showPrimaryItems);
setShowSecondaryItems(DEFAULT_FILTERS_STATE.showSecondaryItems);
setSortingMethod(DEFAULT_FILTERS_STATE.sortingMethod);
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
setFilterUserStatus(DEFAULT_FILTERS_STATE.filterUserStatus);
sendAnalytics("Library", "Reset all filters");
}}
/>
</SubPanel>
),
[
filterUserStatus,
groupingMethod,
hoverable,
keepInfoVisible,
langui,
searchName,
setKeepInfoVisible,
setShowPrimaryItems,
setShowSecondaryItems,
setShowSubitems,
showPrimaryItems,
showSecondaryItems,
showSubitems,
sortingMethod,
toggleKeepInfoVisible,
toggleShowPrimaryItems,
toggleShowSecondaryItems,
toggleShowSubitems,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(items, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<PreviewCard
href={`/library/${item.attributes.slug}`}
title={item.attributes.title}
subtitle={item.attributes.subtitle}
thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="21/29.7"
thumbnailRounded={false}
keepInfoVisible={keepInfoVisible}
topChips={
item.attributes.metadata &&
item.attributes.metadata.length > 0 &&
item.attributes.metadata[0]
? [prettyItemSubType(item.attributes.metadata[0])]
: []
}
bottomChips={item.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
metadata={{
releaseDate: item.attributes.release_date,
price: item.attributes.price,
position: "Bottom",
}}
infoAppend={
!isUntangibleGroupItem(item.attributes.metadata?.[0]) && (
<PreviewCardCTAs id={item.id} />
)
}
/>
)}
className={cJoin(
"grid-cols-2 items-end",
cIf(isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]")
)}
searchingTerm={searchName}
sortingFunction={sortingFunction}
groupingFunction={groupingFunction}
searchingBy={(item) =>
prettyInlineTitle("", item.attributes.title, item.attributes.subtitle)
}
filteringFunction={filteringFunction}
paginationItemPerPage={25}
/>
</ContentPanel>
),
[
filteringFunction,
groupingFunction,
isContentPanelAtLeast4xl,
items,
keepInfoVisible,
searchName,
sortingFunction,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
{...otherProps}
/>
);
};
export default Library;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const items = await sdk.getLibraryItemsPreview({
language_code: context.locale ?? "en",
});
if (!items.libraryItems?.data) return { notFound: true };
const props: Props = {
items: items.libraryItems.data,
openGraph: getOpenGraph(langui, langui.library ?? "Library"),
};
return {
props: props,
};
};

View File

@ -1,49 +0,0 @@
import { GetStaticProps } from "next";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {}
const Merch = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
subPanel={
<SubPanel>
<PanelHeader
icon={Icon.Store}
title={langui.merch}
description={langui.merch_description}
/>
</SubPanel>
}
{...props}
/>
);
};
export default Merch;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = (context) => {
const langui = getLangui(context.locale);
const props: Props = {
openGraph: getOpenGraph(langui, langui.merch ?? "Merch"),
};
return {
props: props,
};
};

View File

@ -1,103 +0,0 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { NextRouter, useRouter } from "next/router";
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { Terminal } from "components/Cli/Terminal";
import { PostWithTranslations } from "types/types";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { prettyTerminalBoxedTitle } from "helpers/terminal";
import { prettyMarkdown } from "helpers/description";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends PostStaticProps {}
const LibrarySlug = (props: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const router = useRouter();
if (isTerminalMode) {
return (
<Terminal
parentPath={"/news"}
childrenPaths={[]}
content={terminalPostPage(props.post, router)}
/>
);
}
return (
<PostPage
returnHref="/news"
returnTitle={langui.news}
displayCredits
displayThumbnailHeader
displayToc
{...props}
/>
);
};
export default LibrarySlug;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const slug =
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
return await getPostStaticProps(slug)(context);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const posts = await sdk.getPostsSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(posts.posts?.data, ["attributes"] as const).map((item) => {
context.locales?.map((local) =>
paths.push({ params: { slug: item.attributes.slug }, locale: local })
);
});
return {
paths,
fallback: "blocking",
};
};
const terminalPostPage = (post: PostWithTranslations, router: NextRouter): string => {
let result = "";
if (router.locales && router.locale) {
const selectedTranslation = staticSmartLanguage({
items: filterHasAttributes(post.translations, ["language.data.attributes.code"] as const),
languageExtractor: (item) => item.language.data.attributes.code,
preferredLanguages: getDefaultPreferredLanguages(router.locale, router.locales),
});
if (selectedTranslation) {
result += prettyTerminalBoxedTitle(selectedTranslation.title);
if (isDefinedAndNotEmpty(selectedTranslation.excerpt)) {
result += "\n\n";
result += prettyMarkdown(selectedTranslation.excerpt);
}
if (isDefinedAndNotEmpty(selectedTranslation.body)) {
result += "\n\n";
result += prettyMarkdown(selectedTranslation.body);
}
}
}
result += "\n\n";
return result;
};

View File

@ -1,206 +0,0 @@
import { GetStaticProps } from "next";
import { useMemo, useState } from "react";
import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { GetPostsPreviewQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters";
import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import { SmartList } from "components/SmartList";
import { getOpenGraph } from "helpers/openGraph";
import { compareDate } from "helpers/date";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
searchName: "",
keepInfoVisible: true,
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"];
}
const News = ({ posts, ...otherProps }: Props): JSX.Element => {
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const {
value: keepInfoVisible,
toggle: toggleKeepInfoVisible,
setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader icon={Icon.Feed} title={langui.news} description={langui.news_description} />
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={(name) => {
setSearchName(name);
if (isDefinedAndNotEmpty(name)) {
sendAnalytics("News", "Change search term");
} else {
sendAnalytics("News", "Clear search term");
}
}}
/>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch
value={keepInfoVisible}
onClick={() => {
toggleKeepInfoVisible();
sendAnalytics("News", `Always ${keepInfoVisible ? "hide" : "show"} info`);
}}
/>
</WithLabel>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
sendAnalytics("News", "Reset all filters");
}}
/>
</SubPanel>
),
[hoverable, keepInfoVisible, langui, searchName, setKeepInfoVisible, toggleKeepInfoVisible]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(posts, ["attributes", "id"] as const)}
getItemId={(post) => post.id}
renderItem={({ item: post }) => (
<TranslatedPreviewCard
href={`/news/${post.attributes.slug}`}
translations={filterHasAttributes(post.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
language: translation.language.data.attributes.code,
title: translation.title,
description: translation.excerpt,
}))}
fallback={{ title: prettySlug(post.attributes.slug) }}
thumbnail={post.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio="3/2"
thumbnailForceAspectRatio
bottomChips={post.attributes.categories?.data.map(
(category) => category.attributes?.short ?? ""
)}
keepInfoVisible={keepInfoVisible}
metadata={{
releaseDate: post.attributes.date,
releaseDateFormat: "long",
position: "Top",
}}
/>
)}
className={cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-4 gap-y-6"
)}
searchingTerm={searchName}
searchingBy={(post) =>
`${prettySlug(post.attributes.slug)} ${post.attributes.translations
?.map((translation) => translation?.title)
.join(" ")}`
}
paginationItemPerPage={25}
/>
</ContentPanel>
),
[keepInfoVisible, posts, searchName, isContentPanelAtLeast4xl]
);
if (isTerminalMode) {
return (
<Terminal
parentPath="/"
childrenPaths={filterHasAttributes(posts, ["attributes"] as const).map(
(post) => post.attributes.slug
)}
/>
);
}
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
{...otherProps}
/>
);
};
export default News;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const posts = await sdk.getPostsPreview();
if (!posts.posts) return { notFound: true };
const props: Props = {
posts: sortPosts(posts.posts.data),
openGraph: getOpenGraph(langui, langui.news ?? "News"),
};
return {
props: props,
};
};
/*
*
* PRIVATE METHODS
*/
const sortPosts = (posts: Props["posts"]): Props["posts"] =>
posts.sort((a, b) => compareDate(a.attributes?.date, b.attributes?.date)).reverse();

View File

@ -1,318 +0,0 @@
import { useCallback, useMemo } from "react";
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Chip } from "components/Chip";
import { HorizontalLine } from "components/HorizontalLine";
import { Img } from "components/Img";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import DefinitionCard from "components/Wiki/DefinitionCard";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { WikiPageWithTranslations } from "types/types";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { prettySlug, sJoin } from "helpers/formatters";
import { ImageQuality } from "helpers/img";
import { getOpenGraph } from "helpers/openGraph";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { getDescription } from "helpers/description";
import { cIf, cJoin } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Terminal } from "components/Cli/Terminal";
import { prettyTerminalBoxedTitle, prettyTerminalUnderlinedTitle } from "helpers/terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
page: WikiPageWithTranslations;
}
const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const router = useRouter();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const { showLightBox } = useAtomGetter(atoms.lightBox);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: page.translations,
languageExtractor: useCallback(
(item: NonNullable<Props["page"]["translations"][number]>) =>
item.language?.data?.attributes?.code,
[]
),
});
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton href={`/wiki`} title={langui.wiki} displayOnlyOn={"3ColumnsLayout"} />
</SubPanel>
),
[langui]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Large}>
<ReturnButton
href={`/wiki`}
title={langui.wiki}
displayOnlyOn={"1ColumnLayout"}
className="mb-10"
/>
<div className="flex flex-wrap place-content-center gap-3">
<h1 className="text-center text-3xl">{selectedTranslation?.title}</h1>
{selectedTranslation?.aliases && selectedTranslation.aliases.length > 0 && (
<p className="mr-3 text-center text-2xl">
{`(${selectedTranslation.aliases.map((alias) => alias?.alias).join("・")})`}
</p>
)}
<LanguageSwitcher {...languageSwitcherProps} />
</div>
{selectedTranslation && (
<>
<HorizontalLine />
<div className="text-justify">
<div
className={cJoin(
"mb-8 overflow-hidden rounded-lg bg-mid text-center",
cIf(is3ColumnsLayout, "float-right ml-8 w-96")
)}>
{page.thumbnail?.data?.attributes && (
<Img
src={page.thumbnail.data.attributes}
quality={ImageQuality.Medium}
className="w-full cursor-pointer"
onClick={() => {
if (page.thumbnail?.data?.attributes) {
showLightBox([page.thumbnail.data.attributes]);
}
}}
/>
)}
<div className="my-4 grid gap-4 p-4">
{page.categories?.data && page.categories.data.length > 0 && (
<>
<p className="font-headers text-xl font-bold">{langui.categories}</p>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(page.categories.data, ["attributes"] as const).map(
(category) => (
<Chip key={category.id} text={category.attributes.name} />
)
)}
</div>
</>
)}
{page.tags?.data && page.tags.data.length > 0 && (
<>
<p className="font-headers text-xl font-bold">{langui.tags}</p>
<div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(page.tags.data, ["attributes"] as const).map((tag) => (
<Chip
key={tag.id}
text={
tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
}
/>
))}
</div>
</>
)}
</div>
</div>
{isDefinedAndNotEmpty(selectedTranslation.summary) && (
<div className="mb-12">
<p className="font-headers text-lg font-bold">{langui.summary}</p>
<p>{selectedTranslation.summary}</p>
</div>
)}
{filterHasAttributes(page.definitions, ["translations"] as const).map(
(definition, index) => (
<div key={index} className="mb-12">
<DefinitionCard
source={{
name: definition.source?.data?.attributes?.name,
url: definition.source?.data?.attributes?.content?.data?.attributes?.slug
? sJoin(
"/contents/",
definition.source.data.attributes.content.data.attributes.slug
)
: cJoin(
"/library/",
definition.source?.data?.attributes?.ranged_content?.data?.attributes
?.library_item?.data?.attributes?.slug
),
}}
translations={definition.translations.map((translation) => ({
language: translation?.language?.data?.attributes?.code,
definition: translation?.definition,
status: translation?.status,
}))}
index={index + 1}
categories={filterHasAttributes(definition.categories?.data, [
"attributes",
] as const).map((category) => category.attributes.short)}
/>
</div>
)
)}
</div>
</>
)}
</ContentPanel>
),
[
LanguageSwitcher,
is3ColumnsLayout,
languageSwitcherProps,
langui.categories,
langui.summary,
langui.tags,
langui.wiki,
page.categories?.data,
page.definitions,
page.tags?.data,
page.thumbnail?.data?.attributes,
selectedTranslation,
showLightBox,
]
);
if (isTerminalMode) {
return (
<Terminal
childrenPaths={[]}
parentPath={"/wiki"}
content={`${prettyTerminalBoxedTitle(
`${selectedTranslation?.title}${
selectedTranslation?.aliases && selectedTranslation.aliases.length > 0
? ` (${selectedTranslation.aliases.map((alias) => alias?.alias).join(", ")})`
: ""
}`
)}${
isDefinedAndNotEmpty(selectedTranslation?.summary)
? `${prettyTerminalUnderlinedTitle(langui.summary)}${selectedTranslation?.summary}`
: ""
}${
page.definitions && page.definitions.length > 0
? `${filterHasAttributes(page.definitions, ["translations"] as const).map(
(definition, index) =>
`${prettyTerminalUnderlinedTitle(`${langui.definition} ${index + 1}`)}${
staticSmartLanguage({
items: filterHasAttributes(definition.translations, [
"language.data.attributes.code",
] as const),
languageExtractor: (item) => item.language.data.attributes.code,
preferredLanguages: getDefaultPreferredLanguages(
router.locale ?? "en",
router.locales ?? ["en"]
),
})?.definition
}`
)}`
: ""
}${
isDefinedAndNotEmpty(selectedTranslation?.body?.body)
? `\n\n${selectedTranslation?.body?.body}`
: "\n"
}`}
/>
);
}
return <AppLayout subPanel={subPanel} contentPanel={contentPanel} {...otherProps} />;
};
export default WikiPage;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const slug =
context.params && isDefined(context.params.slug) ? context.params.slug.toString() : "";
const page = await sdk.getWikiPage({
language_code: context.locale ?? "en",
slug: slug,
});
if (!page.wikiPages?.data[0]?.attributes?.translations) return { notFound: true };
const { title, description } = (() => {
const chipsGroups = {
[langui.tags ?? "Tags"]: filterHasAttributes(page.wikiPages.data[0].attributes.tags?.data, [
"attributes",
] as const).map(
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
),
[langui.categories ?? "Categories"]: filterHasAttributes(
page.wikiPages.data[0].attributes.categories?.data,
["attributes"] as const
).map((category) => category.attributes.short),
};
if (context.locale && context.locales) {
const selectedTranslation = staticSmartLanguage({
items: page.wikiPages.data[0].attributes.translations,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
});
if (selectedTranslation) {
return {
title: selectedTranslation.title,
description: getDescription(selectedTranslation.summary, chipsGroups),
};
}
}
return {
title: prettySlug(page.wikiPages.data[0].attributes.slug),
description: getDescription(undefined, chipsGroups),
};
})();
const thumbnail = page.wikiPages.data[0].attributes.thumbnail?.data?.attributes;
const props: Props = {
page: page.wikiPages.data[0].attributes as WikiPageWithTranslations,
openGraph: getOpenGraph(langui, title, description, thumbnail),
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getWikiPagesSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(contents.wikiPages?.data, ["attributes"] as const).map((wikiPage) => {
context.locales?.map((local) =>
paths.push({
params: { slug: wikiPage.attributes.slug },
locale: local,
})
);
});
return {
paths,
fallback: "blocking",
};
};

View File

@ -1,393 +0,0 @@
import { GetStaticProps } from "next";
import { Fragment, useCallback, useMemo } from "react";
import { useRouter } from "next/router";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { InsetBox } from "components/Containers/InsetBox";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import {
Enum_Componenttranslationschronologyitem_Status,
GetChronologyItemsQuery,
GetErasQuery,
} from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters";
import {
filterHasAttributes,
getStatusDescription,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/others";
import { getOpenGraph } from "helpers/openGraph";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { ToolTip } from "components/ToolTip";
import { Chip } from "components/Chip";
import { Ico, Icon } from "components/Ico";
import { AnchorShare } from "components/AnchorShare";
import { datePickerToDate } from "helpers/date";
import { TranslatedProps } from "types/TranslatedProps";
import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
chronologyItems: NonNullable<GetChronologyItemsQuery["chronologyItems"]>["data"];
chronologyEras: NonNullable<GetErasQuery["chronologyEras"]>["data"];
}
const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const ids = useMemo(
() =>
filterHasAttributes(chronologyEras, ["attributes"] as const).map(
(era) => era.attributes.slug
),
[chronologyEras]
);
const currentIntersection = useIntersectionList(ids);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton href="/wiki" title={langui.wiki} displayOnlyOn="3ColumnsLayout" />
<HorizontalLine />
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map((era, index) => (
<Fragment key={era.id}>
<TranslatedNavOption
translations={filterHasAttributes(era.attributes.title, [
"language.data.attributes.code",
] as const).map((translation) => ({
language: translation.language.data.attributes.code,
title: translation.title,
subtitle: `${era.attributes.starting_year}${era.attributes.ending_year}`,
}))}
fallback={{
title: prettySlug(era.attributes.slug),
subtitle: `${era.attributes.starting_year}${era.attributes.ending_year}`,
}}
url={`#${era.attributes.slug}`}
border
active={currentIntersection === index}
/>
</Fragment>
))}
</SubPanel>
),
[chronologyEras, currentIntersection, langui]
);
const contentPanel = useMemo(
() => (
<ContentPanel>
<ReturnButton
href="/wiki"
title={langui.wiki}
displayOnlyOn="1ColumnLayout"
className="mb-10"
/>
{filterHasAttributes(chronologyEras, ["attributes"] as const).map((era) => (
<TranslatedChronologyEra
key={era.attributes.slug}
id={era.attributes.slug}
translations={filterHasAttributes(era.attributes.title, [
"language.data.attributes.code",
] as const).map((translation) => ({
language: translation.language.data.attributes.code,
title: translation.title,
description: translation.description,
}))}
fallback={{ title: prettySlug(era.attributes.slug) }}
chronologyItems={filterHasAttributes(chronologyItems, ["attributes"] as const).filter(
(item) =>
item.attributes.year >= era.attributes.starting_year &&
item.attributes.year < era.attributes.ending_year
)}
/>
))}
</ContentPanel>
),
[chronologyEras, chronologyItems, langui]
);
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
};
export default Chronology;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const chronologyItems = await sdk.getChronologyItems();
const chronologyEras = await sdk.getEras();
if (!chronologyItems.chronologyItems || !chronologyEras.chronologyEras) return { notFound: true };
const props: Props = {
chronologyItems: chronologyItems.chronologyItems.data,
chronologyEras: chronologyEras.chronologyEras.data,
openGraph: getOpenGraph(langui, langui.chronology ?? "Chronology"),
};
return {
props: props,
};
};
/*
*
* PRIVATE COMPONENTS
*/
interface ChronologyEraProps {
id: string;
title: string;
description?: string | null | undefined;
chronologyItems: Props["chronologyItems"];
}
const ChronologyEra = ({ id, title, description, chronologyItems }: ChronologyEraProps) => {
const yearGroups = useMemo(() => {
const memo: Props["chronologyItems"][] = [];
let currentYear = -Infinity;
filterHasAttributes(chronologyItems, ["attributes"] as const).forEach((item) => {
if (currentYear === item.attributes.year) {
memo[memo.length - 1].push(item);
} else {
currentYear = item.attributes.year;
memo.push([item]);
}
});
return memo;
}, [chronologyItems]);
return (
<div id={id}>
<InsetBox className="my-8 grid gap-4 text-center">
<h2 className="flex place-content-center gap-3 text-2xl">
{title}
<AnchorShare id={id} />
</h2>
{isDefinedAndNotEmpty(description) && <p className="whitespace-pre-line ">{description}</p>}
</InsetBox>
<div>
{yearGroups.map((item, index) => (
<ChronologyYear key={index} items={item} />
))}
</div>
</div>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const TranslatedChronologyEra = ({
translations,
fallback,
...otherProps
}: TranslatedProps<Parameters<typeof ChronologyEra>[0], "description" | "title">): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languageExtractor: (item: { language: string }): string => item.language,
});
return (
<ChronologyEra
title={selectedTranslation?.title ?? fallback.title}
description={selectedTranslation?.description ?? fallback.description}
{...otherProps}
/>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyYearProps {
items: NonNullable<Props["chronologyItems"]>;
}
const ChronologyYear = ({ items }: ChronologyYearProps) => (
<div
className="rounded-2xl target:my-4 target:bg-mid target:py-4"
id={generateAnchor(items[0].attributes?.year)}>
{filterHasAttributes(items, ["attributes.events"] as const).map((item, index) => (
<ChronologyDate
key={index}
date={{
year: item.attributes.year,
month: item.attributes.month,
day: item.attributes.day,
displayYear: index === 0,
overwriteYear: item.attributes.displayed_date,
}}
events={item.attributes.events}
/>
))}
</div>
);
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyDateProps {
date: {
year: number;
month: number | null | undefined;
day: number | null | undefined;
displayYear: boolean;
overwriteYear?: string | null | undefined;
};
events: NonNullable<
NonNullable<NonNullable<Props["chronologyItems"]>[number]["attributes"]>["events"]
>;
}
export const ChronologyDate = ({ date, events }: ChronologyDateProps): JSX.Element => {
const router = useRouter();
return (
<div
className="grid grid-cols-[4em] grid-rows-[auto_1fr]
gap-x-8 rounded-2xl py-4 px-8 target:my-4 target:bg-mid target:py-8"
id={generateAnchor(date.year, date.month, date.day)}>
{date.displayYear && (
<p className="mt-5 text-right text-lg font-bold">
{isDefinedAndNotEmpty(date.overwriteYear) ? date.overwriteYear : date.year}
</p>
)}
<p className="col-start-1 text-right text-sm text-dark">
{isDefined(date.month)
? isDefined(date.day)
? datePickerToDate({
year: date.year,
month: date.month,
day: date.day,
}).toLocaleDateString(router.locale, {
month: "short",
day: "numeric",
})
: datePickerToDate({
year: date.year,
month: date.month,
day: date.day,
}).toLocaleDateString(router.locale, {
month: "short",
})
: ""}
</p>
<div className="col-start-2 row-span-2 row-start-1 grid gap-4">
{filterHasAttributes(events, ["id", "translations"] as const).map((event) => (
<ChronologyEvent
id={generateAnchor(date.year, date.month, date.day)}
key={event.id}
event={event}
/>
))}
</div>
</div>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface ChronologyEventProps {
event: NonNullable<
NonNullable<
NonNullable<NonNullable<Props["chronologyItems"]>[number]["attributes"]>["events"]
>[number]
>;
id: string;
}
export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Element => {
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: event.translations ?? [],
languageExtractor: useCallback(
(item: NonNullable<ChronologyEventProps["event"]["translations"]>[number]) =>
item?.language?.data?.attributes?.code,
[]
),
});
return (
<div>
{selectedTranslation && (
<>
<div className="mr-2 flex place-items-center gap-x-2">
<LanguageSwitcher {...languageSwitcherProps} size="small" showBadge={false} />
{selectedTranslation.status !==
Enum_Componenttranslationschronologyitem_Status.Done && (
<ToolTip
content={getStatusDescription(selectedTranslation.status, langui)}
maxWidth={"20rem"}>
<Chip text={selectedTranslation.status} />
</ToolTip>
)}
<p className="grid grid-flow-col gap-1 place-self-start text-xs leading-6 text-dark">
{event.source?.data ? (
`(${event.source.data.attributes?.name})`
) : (
<div className="flex items-center gap-1">
<Ico icon={Icon.Warning} className="!text-sm" />
{langui.no_source_warning}
</div>
)}
</p>
<span className="flex-shrink">
<AnchorShare id={id} />
</span>
</div>
{selectedTranslation.title && (
<div className="mt-1 flex place-content-start place-items-start gap-2">
<h3 className="font-headers font-bold">{selectedTranslation.title}</h3>
</div>
)}
{selectedTranslation.description && (
<p className="whitespace-pre-line">{selectedTranslation.description}</p>
)}
{selectedTranslation.note && <em>{`${langui.notes}: ${selectedTranslation.note}`}</em>}
</>
)}
</div>
);
};
/*
*
* PRIVATE METHODS
*/
const generateAnchor = (
year: number | undefined,
month?: number | null | undefined,
day?: number | null | undefined
): string => {
let result = "";
if (isDefined(year)) result += year;
if (isDefined(month)) result += `-${month.toString().padStart(2, "0")}`;
if (isDefined(day)) result += `-${day.toString().padStart(2, "0")}`;
return result;
};

View File

@ -1,289 +0,0 @@
import { GetStaticProps } from "next";
import { useCallback, useMemo, useState } from "react";
import { useBoolean } from "usehooks-ts";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { NavOption } from "components/PanelComponents/NavOption";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getReadySdk } from "graphql/sdk";
import { GetWikiPageQuery, GetWikiPagesPreviewsQuery } from "graphql/generated";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { HorizontalLine } from "components/HorizontalLine";
import { Button } from "components/Inputs/Button";
import { Switch } from "components/Inputs/Switch";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { filterDefined, filterHasAttributes, isDefinedAndNotEmpty } from "helpers/others";
import { SmartList } from "components/SmartList";
import { Select } from "components/Inputs/Select";
import { SelectiveNonNullable } from "types/SelectiveNonNullable";
import { prettySlug } from "helpers/formatters";
import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
searchName: "",
keepInfoVisible: true,
groupingMethod: -1,
};
/*
*
* PAGE
*/
interface Props extends AppLayoutRequired {
pages: NonNullable<GetWikiPagesPreviewsQuery["wikiPages"]>["data"];
}
const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod
);
const {
value: keepInfoVisible,
toggle: toggleKeepInfoVisible,
setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.TravelExplore}
title={langui.wiki}
description={langui.wiki_description}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={(name) => {
setSearchName(name);
if (isDefinedAndNotEmpty(name)) {
sendAnalytics("Wiki", "Change search term");
} else {
sendAnalytics("Wiki", "Clear search term");
}
}}
/>
<WithLabel label={langui.group_by}>
<Select
className="w-full"
options={[langui.category ?? "Category"]}
value={groupingMethod}
onChange={(value) => {
setGroupingMethod(value);
sendAnalytics("Wiki", `Change grouping method (${["none", "category"][value + 1]})`);
}}
allowEmpty
/>
</WithLabel>
{hoverable && (
<WithLabel label={langui.always_show_info}>
<Switch
value={keepInfoVisible}
onClick={() => {
toggleKeepInfoVisible();
sendAnalytics("Wiki", `Always ${keepInfoVisible ? "hide" : "show"} info`);
}}
/>
</WithLabel>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
sendAnalytics("Wiki", "Reset all filters");
}}
/>
<HorizontalLine />
<p className="mb-4 font-headers text-xl font-bold">{langui.special_pages}</p>
<NavOption title={langui.chronology} url="/wiki/chronology" border />
</SubPanel>
),
[
groupingMethod,
hoverable,
keepInfoVisible,
langui,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
);
const groupingFunction = useCallback(
(
item: SelectiveNonNullable<
NonNullable<GetWikiPageQuery["wikiPages"]>["data"][number],
"attributes" | "id"
>
): string[] => {
switch (groupingMethod) {
case 0: {
const categories = filterHasAttributes(item.attributes.categories?.data, [
"attributes",
] as const);
if (categories.length > 0) {
return categories.map((category) => category.attributes.name);
}
return [langui.no_category ?? "No category"];
}
default: {
return [""];
}
}
},
[groupingMethod, langui]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(pages, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<TranslatedPreviewCard
href={`/wiki/${item.attributes.slug}`}
translations={filterHasAttributes(item.attributes.translations, [
"language.data.attributes.code",
] as const).map((translation) => ({
title: translation.title,
subtitle:
translation.aliases && translation.aliases.length > 0
? translation.aliases.map((alias) => alias?.alias).join("・")
: undefined,
description: translation.summary,
language: translation.language.data.attributes.code,
}))}
fallback={{ title: prettySlug(item.attributes.slug) }}
thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnailAspectRatio={"4/3"}
thumbnailRounded
thumbnailForceAspectRatio
keepInfoVisible={keepInfoVisible}
topChips={filterHasAttributes(item.attributes.tags?.data, [
"attributes",
] as const).map(
(tag) => tag.attributes.titles?.[0]?.title ?? prettySlug(tag.attributes.slug)
)}
bottomChips={filterHasAttributes(item.attributes.categories?.data, [
"attributes",
] as const).map((category) => category.attributes.short)}
/>
)}
className={cIf(
isContentPanelAtLeast4xl,
"grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-x-6 gap-y-8",
"grid-cols-2 gap-x-3 gap-y-5"
)}
searchingTerm={searchName}
searchingBy={(item) =>
filterDefined(item.attributes.translations)
.map(
(translation) =>
`${translation.title} ${filterDefined(translation.aliases)
.map((alias) => alias.alias)
.join(" ")}`
)
.join(" ")
}
groupingFunction={groupingFunction}
paginationItemPerPage={25}
/>
</ContentPanel>
),
[groupingFunction, keepInfoVisible, pages, searchName, isContentPanelAtLeast4xl]
);
if (isTerminalMode) {
return (
<Terminal
parentPath="/"
childrenPaths={filterHasAttributes(pages, ["attributes"] as const).map(
(page) => page.attributes.slug
)}
/>
);
}
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
{...otherProps}
/>
);
};
export default Wiki;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const langui = getLangui(context.locale);
const pages = await sdk.getWikiPagesPreviews({
language_code: context.locale ?? "en",
});
if (!pages.wikiPages?.data) return { notFound: true };
const props: Props = {
pages: sortPages(pages.wikiPages.data),
openGraph: getOpenGraph(langui, langui.wiki ?? "Wiki"),
};
return {
props: props,
};
};
/*
*
* PRIVATE METHODS
*/
const sortPages = (pages: Props["pages"]): Props["pages"] =>
pages.sort((a, b) => {
const slugA = a.attributes?.slug ?? "";
const slugB = b.attributes?.slug ?? "";
return slugA.localeCompare(slugB);
});

View File

@ -2,10 +2,6 @@
@tailwind components;
@tailwind utilities;
#__next {
@apply bg-light font-body font-medium text-black;
}
* {
@apply box-border scroll-m-[40vh] scroll-smooth scrollbar-thin ![-webkit-tap-highlight-color:transparent];
}

20
src/types/next.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
export {};
declare global {
type Params = Record<string, string>;
type SearchParams = Record<string, string>;
type Layout = (props: { children: React.ReactNode; params: Params }) => JSX.Element;
type Template = Layout;
type Page = (props: { params: Params; searchParams: SearchParams }) => JSX.Element;
type Loading = () => JSX.Element;
type NotFound = () => Loading;
type Head = (props: { params: Params }) => JSX.Element;
type Error = (props: { error: Error; reset: () => void }) => JSX.Element;
type GenerateStaticParams = () => Record<string, string[] | string>[];
}

View File

@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES6",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"importHelpers": true,
"allowJs": true,
"skipLibCheck": true,
@ -17,7 +21,20 @@
"incremental": true,
"baseUrl": "src"
// "noUncheckedIndexedAccess": true
,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}