Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
DrMint | 8b175054e9 | |
DrMint | efa5ccb537 | |
DrMint | 29f6f6a45c | |
DrMint | 2aea7fa040 | |
DrMint | 28a03a4138 | |
DrMint | 3eb4d64346 |
|
@ -12,6 +12,7 @@ src/graphql/generated.ts
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
src/pages.old
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
.next
|
.next
|
||||||
|
public/local-data
|
||||||
|
|
|
@ -1,17 +1,9 @@
|
||||||
/* CONFIG */
|
|
||||||
|
|
||||||
const locales = ["en", "es", "fr", "pt-br", "ja"];
|
|
||||||
|
|
||||||
/* END CONFIG */
|
|
||||||
|
|
||||||
/* @type {import('next').NextConfig} */
|
/* @type {import('next').NextConfig} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
swcMinify: true,
|
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
i18n: {
|
experimental: {
|
||||||
locales: locales,
|
appDir: true,
|
||||||
defaultLocale: "en",
|
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ["img.accords-library.com", "watch.accords-library.com"],
|
domains: ["img.accords-library.com", "watch.accords-library.com"],
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
|
@ -17,60 +17,60 @@
|
||||||
"prettier": "prettier --end-of-line auto --write ."
|
"prettier": "prettier --end-of-line auto --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/material-icons": "^4.5.4",
|
"@fontsource/material-icons": "4.5.4",
|
||||||
"@fontsource/material-icons-outlined": "^4.5.4",
|
"@fontsource/material-icons-outlined": "4.5.4",
|
||||||
"@fontsource/opendyslexic": "^4.5.4",
|
"@fontsource/opendyslexic": "4.5.4",
|
||||||
"@fontsource/share-tech-mono": "^4.5.9",
|
"@fontsource/share-tech-mono": "4.5.9",
|
||||||
"@fontsource/vollkorn": "^4.5.12",
|
"@fontsource/vollkorn": "4.5.12",
|
||||||
"@fontsource/zen-maru-gothic": "^4.5.13",
|
"@fontsource/zen-maru-gothic": "4.5.13",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "4.2.6",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "10.4.13",
|
||||||
"cuid": "^2.1.8",
|
"graphql-request": "5.0.0",
|
||||||
"graphql-request": "^5.0.0",
|
"jotai": "1.9.1",
|
||||||
"jotai": "^1.9.0",
|
"markdown-to-jsx": "7.1.7",
|
||||||
"markdown-to-jsx": "^7.1.7",
|
"next": "13.0.2",
|
||||||
"next": "^12.3.1",
|
"nodemailer": "6.8.0",
|
||||||
"nodemailer": "^6.8.0",
|
"rc-slider": "10.0.1",
|
||||||
"rc-slider": "^10.0.1",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hotkeys-hook": "^3.4.7",
|
"react-hotkeys-hook": "3.4.7",
|
||||||
"react-swipeable": "^7.0.0",
|
"react-swipeable": "7.0.0",
|
||||||
"react-zoom-pan-pinch": "^2.1.3",
|
"react-zoom-pan-pinch": "2.1.3",
|
||||||
"string-natural-compare": "^3.0.1",
|
"string-natural-compare": "3.0.1",
|
||||||
"throttle-debounce": "^5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"turndown": "^7.1.1",
|
"turndown": "7.1.1",
|
||||||
"ua-parser-js": "^1.0.32",
|
"ua-parser-js": "1.0.32",
|
||||||
"usehooks-ts": "^2.9.1"
|
"usehooks-ts": "2.9.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@digitak/esrun": "^3.2.14",
|
"@digitak/esrun": "3.2.14",
|
||||||
"@graphql-codegen/cli": "^2.13.8",
|
"@graphql-codegen/cli": "2.13.11",
|
||||||
"@graphql-codegen/typescript": "2.8.0",
|
"@graphql-codegen/typescript": "2.8.1",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.7",
|
"@graphql-codegen/typescript-graphql-request": "4.5.8",
|
||||||
"@graphql-codegen/typescript-operations": "^2.5.5",
|
"@graphql-codegen/typescript-operations": "2.5.6",
|
||||||
"@types/node": "18.11.7",
|
"@types/node": "18.11.9",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "6.4.6",
|
||||||
"@types/react": "18.0.22",
|
"@types/react": "18.0.22",
|
||||||
"@types/react-dom": "^18.0.8",
|
"@types/react-dom": "18.0.8",
|
||||||
"@types/string-natural-compare": "^3.0.2",
|
"@types/string-natural-compare": "3.0.2",
|
||||||
"@types/throttle-debounce": "^5.0.0",
|
"@types/throttle-debounce": "5.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "5.0.1",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
"@typescript-eslint/eslint-plugin": "5.42.0",
|
||||||
"@typescript-eslint/parser": "^5.41.0",
|
"@typescript-eslint/parser": "5.42.0",
|
||||||
"dotenv": "^16.0.3",
|
"cuid": "2.1.8",
|
||||||
"eslint": "^8.26.0",
|
"dotenv": "16.0.3",
|
||||||
"eslint-config-next": "12.3.1",
|
"eslint": "8.26.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-config-next": "13.0.2",
|
||||||
"graphql": "^16.6.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"next-sitemap": "^3.1.29",
|
"graphql": "16.6.0",
|
||||||
"prettier": "^2.7.1",
|
"next-sitemap": "3.1.30",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier": "2.7.1",
|
||||||
"tailwindcss": "^3.2.1",
|
"prettier-plugin-tailwindcss": "0.1.13",
|
||||||
"ts-unused-exports": "^8.0.0",
|
"tailwindcss": "3.2.1",
|
||||||
"typescript": "^4.8.4"
|
"ts-unused-exports": "8.0.0",
|
||||||
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"react-zoom-pan-pinch": {
|
"react-zoom-pan-pinch": {
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
const Page: Page = () => <>Hello from contents</>;
|
||||||
|
export default Page;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -234,7 +234,7 @@ export const Terminal = ({
|
||||||
}, [line]);
|
}, [line]);
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
ref={terminalWindowRef}
|
ref={terminalWindowRef}
|
||||||
className="h-full overflow-scroll scroll-auto p-6 scrollbar-none">
|
className="h-full overflow-scroll scroll-auto p-6 scrollbar-none">
|
||||||
|
|
|
@ -32,8 +32,6 @@ export const SettingsPopup = (): JSX.Element => {
|
||||||
|
|
||||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const currencyOptions = useMemo(
|
const currencyOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
filterHasAttributes(currencies, ["attributes"] as const).map(
|
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`,
|
`mt-4 grid justify-items-center gap-16 text-center`,
|
||||||
cIf(!is1ColumnLayout, "grid-cols-[auto_auto]")
|
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
|
<div
|
||||||
className={cJoin(
|
className={cJoin(
|
||||||
"grid place-items-center gap-8 text-center",
|
"grid place-items-center gap-8 text-center",
|
||||||
|
|
|
@ -1,28 +1,17 @@
|
||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { useEffectOnce } from "usehooks-ts";
|
import { useEffectOnce } from "usehooks-ts";
|
||||||
import { atom } from "jotai";
|
|
||||||
import { UploadImageFragment } from "graphql/generated";
|
import { UploadImageFragment } from "graphql/generated";
|
||||||
import { LightBox } from "components/LightBox";
|
import { LightBox } from "components/LightBox";
|
||||||
import { filterDefined } from "helpers/others";
|
import { filterDefined } from "helpers/others";
|
||||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
import { useAtomSetter } from "helpers/atoms";
|
||||||
|
import { lightBox } from "contexts/atoms";
|
||||||
const lightBoxAtom = atomPairing(
|
|
||||||
atom<{
|
|
||||||
showLightBox: (
|
|
||||||
images: (UploadImageFragment | string | null | undefined)[],
|
|
||||||
index?: number
|
|
||||||
) => void;
|
|
||||||
}>({ showLightBox: () => null })
|
|
||||||
);
|
|
||||||
|
|
||||||
export const lightBox = lightBoxAtom[0];
|
|
||||||
|
|
||||||
export const LightBoxProvider = (): JSX.Element => {
|
export const LightBoxProvider = (): JSX.Element => {
|
||||||
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
||||||
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
||||||
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
||||||
|
|
||||||
const setShowLightBox = useAtomSetter(lightBoxAtom);
|
const setShowLightBox = useAtomSetter(lightBox);
|
||||||
|
|
||||||
useEffectOnce(() =>
|
useEffectOnce(() =>
|
||||||
setShowLightBox({
|
setShowLightBox({
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
|
||||||
import { useAtomSetter } from "helpers/atoms";
|
import { useAtomSetter } from "helpers/atoms";
|
||||||
import { atoms } from "contexts/atoms";
|
import { atoms } from "contexts/atoms";
|
||||||
|
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
||||||
|
|
||||||
export const useAppLayout = (): void => {
|
export const useAppLayout = (): void => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
@ -4,13 +4,24 @@ import { localData } from "contexts/localData";
|
||||||
import { containerQueries } from "contexts/containerQueries";
|
import { containerQueries } from "contexts/containerQueries";
|
||||||
import { atomPairing } from "helpers/atoms";
|
import { atomPairing } from "helpers/atoms";
|
||||||
import { settings } from "contexts/settings";
|
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
|
* I'm getting a weird error if I put those atoms in appLayout.ts
|
||||||
* So I'm putting the atoms here. Sucks, I know.
|
* 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 ] */
|
/* [ APPLAYOUT ATOMS ] */
|
||||||
|
|
||||||
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
|
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
|
||||||
|
@ -44,6 +55,6 @@ export const atoms = {
|
||||||
layout,
|
layout,
|
||||||
terminal,
|
terminal,
|
||||||
localData,
|
localData,
|
||||||
lightBox,
|
lightBox: lightBox[0],
|
||||||
containerQueries,
|
containerQueries,
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,7 +35,6 @@ export const useLocalData = (): void => {
|
||||||
const setCurrencies = useAtomSetter(currencies);
|
const setCurrencies = useAtomSetter(currencies);
|
||||||
const setLangui = useAtomSetter(langui);
|
const setLangui = useAtomSetter(langui);
|
||||||
|
|
||||||
const { locale } = useRouter();
|
|
||||||
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
||||||
const { data: rawCurrencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
|
const { data: rawCurrencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
|
||||||
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
||||||
|
@ -54,6 +53,6 @@ export const useLocalData = (): void => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[useLocalData] Refresh langui");
|
console.log("[useLocalData] Refresh langui");
|
||||||
setLangui(processLangui(rawLangui, locale));
|
setLangui(processLangui(rawLangui, "en"));
|
||||||
}, [locale, rawLangui, setLangui]);
|
}, [rawLangui, setLangui]);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { useRouter } from "next/router";
|
// import { useRouter } from "next/router";
|
||||||
import { useLayoutEffect, useEffect } from "react";
|
import { useLayoutEffect, useEffect } from "react";
|
||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { atomWithStorage } from "jotai/utils";
|
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 { getDefaultPreferredLanguages } from "helpers/locales";
|
||||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||||
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
||||||
|
import { Ids } from "types/ids";
|
||||||
|
|
||||||
export enum ThemeMode {
|
export enum ThemeMode {
|
||||||
Dark = "dark",
|
Dark = "dark",
|
||||||
|
@ -32,11 +33,10 @@ export const settings = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSettings = (): void => {
|
export const useSettings = (): void => {
|
||||||
const router = useRouter();
|
// const router = useRouter();
|
||||||
const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
|
// const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
|
||||||
const fontSize = useAtomGetter(fontSizeAtom);
|
const fontSize = useAtomGetter(fontSizeAtom);
|
||||||
const isDyslexic = useAtomGetter(dyslexicAtom);
|
const setDarkMode = useAtomSetter(darkModeAtom);
|
||||||
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
|
|
||||||
const themeMode = useAtomGetter(themeModeAtom);
|
const themeMode = useAtomGetter(themeModeAtom);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
@ -46,19 +46,6 @@ export const useSettings = (): void => {
|
||||||
}
|
}
|
||||||
}, [fontSize]);
|
}, [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 */
|
/* DARK MODE */
|
||||||
const prefersDarkMode = usePrefersDarkMode();
|
const prefersDarkMode = usePrefersDarkMode();
|
||||||
|
|
||||||
|
@ -66,31 +53,21 @@ export const useSettings = (): void => {
|
||||||
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
|
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
|
||||||
}, [prefersDarkMode, setDarkMode, themeMode]);
|
}, [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 */
|
/* PREFERRED LANGUAGES */
|
||||||
|
|
||||||
|
/*
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (preferredLanguages.length === 0) {
|
if (preferredLanguages.length === 0) {
|
||||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
||||||
}
|
}
|
||||||
} else if (router.locale !== preferredLanguages[0]) {
|
} else if (router.locale !== preferredLanguages[0]) {
|
||||||
|
*/
|
||||||
/*
|
/*
|
||||||
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
||||||
* different preferredLanguages[0] from router.locale
|
* different preferredLanguages[0] from router.locale
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
setTimeout(
|
setTimeout(
|
||||||
async () =>
|
async () =>
|
||||||
router.replace(router.asPath, router.asPath, {
|
router.replace(router.asPath, router.asPath, {
|
||||||
|
@ -100,4 +77,5 @@ export const useSettings = (): void => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [preferredLanguages, router, setPreferredLanguages]);
|
}, [preferredLanguages, router, setPreferredLanguages]);
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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;
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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");
|
|
|
@ -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");
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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");
|
|
|
@ -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");
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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",
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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",
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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",
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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 “x”</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’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’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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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>
|
|
||||||
);
|
|
|
@ -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", "0")
|
|
||||||
.replaceAll("1", "1")
|
|
||||||
.replaceAll("2", "2")
|
|
||||||
.replaceAll("3", "3")
|
|
||||||
.replaceAll("4", "4")
|
|
||||||
.replaceAll("5", "5")
|
|
||||||
.replaceAll("6", "6")
|
|
||||||
.replaceAll("7", "7")
|
|
||||||
.replaceAll("8", "8")
|
|
||||||
.replaceAll("9", "9")
|
|
||||||
// Uppercase letters
|
|
||||||
.replaceAll("A", "A")
|
|
||||||
.replaceAll("B", "B")
|
|
||||||
.replaceAll("C", "C")
|
|
||||||
.replaceAll("D", "D")
|
|
||||||
.replaceAll("E", "E")
|
|
||||||
.replaceAll("F", "F")
|
|
||||||
.replaceAll("G", "G")
|
|
||||||
.replaceAll("H", "H")
|
|
||||||
.replaceAll("I", "I")
|
|
||||||
.replaceAll("J", "J")
|
|
||||||
.replaceAll("K", "K")
|
|
||||||
.replaceAll("L", "L")
|
|
||||||
.replaceAll("M", "M")
|
|
||||||
.replaceAll("N", "N")
|
|
||||||
.replaceAll("O", "O")
|
|
||||||
.replaceAll("P", "P")
|
|
||||||
.replaceAll("Q", "Q")
|
|
||||||
.replaceAll("R", "R")
|
|
||||||
.replaceAll("S", "S")
|
|
||||||
.replaceAll("T", "T")
|
|
||||||
.replaceAll("U", "U")
|
|
||||||
.replaceAll("V", "V")
|
|
||||||
.replaceAll("W", "W")
|
|
||||||
.replaceAll("X", "X")
|
|
||||||
.replaceAll("Y", "Y")
|
|
||||||
.replaceAll("Z", "Z")
|
|
||||||
// Lowercase letters
|
|
||||||
.replaceAll("a", "a")
|
|
||||||
.replaceAll("b", "b")
|
|
||||||
.replaceAll("c", "c")
|
|
||||||
.replaceAll("d", "d")
|
|
||||||
.replaceAll("e", "e")
|
|
||||||
.replaceAll("f", "f")
|
|
||||||
.replaceAll("g", "g")
|
|
||||||
.replaceAll("h", "h")
|
|
||||||
.replaceAll("i", "i")
|
|
||||||
.replaceAll("j", "j")
|
|
||||||
.replaceAll("k", "k")
|
|
||||||
.replaceAll("l", "l")
|
|
||||||
.replaceAll("m", "m")
|
|
||||||
.replaceAll("n", "n")
|
|
||||||
.replaceAll("o", "o")
|
|
||||||
.replaceAll("p", "p")
|
|
||||||
.replaceAll("q", "q")
|
|
||||||
.replaceAll("r", "r")
|
|
||||||
.replaceAll("s", "s")
|
|
||||||
.replaceAll("t", "t")
|
|
||||||
.replaceAll("u", "u")
|
|
||||||
.replaceAll("v", "v")
|
|
||||||
.replaceAll("w", "w")
|
|
||||||
.replaceAll("x", "x")
|
|
||||||
.replaceAll("y", "y")
|
|
||||||
.replaceAll("z", "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 ⟹ 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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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’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");
|
|
|
@ -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
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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();
|
|
|
@ -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",
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -2,10 +2,6 @@
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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];
|
@apply box-border scroll-m-[40vh] scroll-smooth scrollbar-thin ![-webkit-tap-highlight-color:transparent];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>[];
|
||||||
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
@ -17,7 +21,20 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"baseUrl": "src"
|
"baseUrl": "src"
|
||||||
// "noUncheckedIndexedAccess": true
|
// "noUncheckedIndexedAccess": true
|
||||||
},
|
,
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"plugins": [
|
||||||
"exclude": ["node_modules"]
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue