Testing add directory
This commit is contained in:
parent
efa5ccb537
commit
8b175054e9
|
@ -12,6 +12,7 @@ src/graphql/generated.ts
|
|||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
src/pages.old
|
||||
|
||||
# production
|
||||
/build
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
/* CONFIG */
|
||||
|
||||
const locales = ["en", "es", "fr", "pt-br", "ja"];
|
||||
|
||||
/* END CONFIG */
|
||||
|
||||
/* @type {import('next').NextConfig} */
|
||||
module.exports = {
|
||||
swcMinify: true,
|
||||
reactStrictMode: true,
|
||||
poweredByHeader: false,
|
||||
i18n: {
|
||||
locales: locales,
|
||||
defaultLocale: "en",
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
images: {
|
||||
domains: ["img.accords-library.com", "watch.accords-library.com"],
|
||||
|
|
|
@ -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;
|
|
@ -32,8 +32,6 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
|
||||
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const currencyOptions = useMemo(
|
||||
() =>
|
||||
filterHasAttributes(currencies, ["attributes"] as const).map(
|
||||
|
@ -66,34 +64,6 @@ export const SettingsPopup = (): JSX.Element => {
|
|||
`mt-4 grid justify-items-center gap-16 text-center`,
|
||||
cIf(!is1ColumnLayout, "grid-cols-[auto_auto]")
|
||||
)}>
|
||||
{router.locales && (
|
||||
<div>
|
||||
<h3 className="text-xl">{langui.languages}</h3>
|
||||
{preferredLanguages.length > 0 && (
|
||||
<OrderableList
|
||||
items={preferredLanguages.map((locale) => ({
|
||||
code: locale,
|
||||
name: prettyLanguage(locale, languages),
|
||||
}))}
|
||||
insertLabels={[
|
||||
{
|
||||
insertAt: 0,
|
||||
name: langui.primary_language ?? "Primary language",
|
||||
},
|
||||
{
|
||||
insertAt: 1,
|
||||
name: langui.secondary_language ?? "Secondary languages",
|
||||
},
|
||||
]}
|
||||
onChange={(items) => {
|
||||
const newPreferredLanguages = items.map((item) => item.code);
|
||||
setPreferredLanguages(newPreferredLanguages);
|
||||
sendAnalytics("Settings", "Change preferred languages");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid place-items-center gap-8 text-center",
|
||||
|
|
|
@ -1,28 +1,17 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
import { useEffectOnce } from "usehooks-ts";
|
||||
import { atom } from "jotai";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
import { LightBox } from "components/LightBox";
|
||||
import { filterDefined } from "helpers/others";
|
||||
import { atomPairing, useAtomSetter } from "helpers/atoms";
|
||||
|
||||
const lightBoxAtom = atomPairing(
|
||||
atom<{
|
||||
showLightBox: (
|
||||
images: (UploadImageFragment | string | null | undefined)[],
|
||||
index?: number
|
||||
) => void;
|
||||
}>({ showLightBox: () => null })
|
||||
);
|
||||
|
||||
export const lightBox = lightBoxAtom[0];
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { lightBox } from "contexts/atoms";
|
||||
|
||||
export const LightBoxProvider = (): JSX.Element => {
|
||||
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
|
||||
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
|
||||
const [lightBoxIndex, setLightBoxIndex] = useState(0);
|
||||
|
||||
const setShowLightBox = useAtomSetter(lightBoxAtom);
|
||||
const setShowLightBox = useAtomSetter(lightBox);
|
||||
|
||||
useEffectOnce(() =>
|
||||
setShowLightBox({
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
||||
import { useAtomSetter } from "helpers/atoms";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useScrollIntoView } from "hooks/useScrollIntoView";
|
||||
|
||||
export const useAppLayout = (): void => {
|
||||
const router = useRouter();
|
||||
|
|
|
@ -4,13 +4,24 @@ import { localData } from "contexts/localData";
|
|||
import { containerQueries } from "contexts/containerQueries";
|
||||
import { atomPairing } from "helpers/atoms";
|
||||
import { settings } from "contexts/settings";
|
||||
import { lightBox } from "contexts/LightBoxProvider";
|
||||
import { UploadImageFragment } from "graphql/generated";
|
||||
|
||||
/*
|
||||
* I'm getting a weird error if I put those atoms in appLayout.ts
|
||||
* So I'm putting the atoms here. Sucks, I know.
|
||||
*/
|
||||
|
||||
/* [ LIGHTBOX ] */
|
||||
|
||||
export const lightBox = atomPairing(
|
||||
atom<{
|
||||
showLightBox: (
|
||||
images: (UploadImageFragment | string | null | undefined)[],
|
||||
index?: number
|
||||
) => void;
|
||||
}>({ showLightBox: () => null })
|
||||
);
|
||||
|
||||
/* [ APPLAYOUT ATOMS ] */
|
||||
|
||||
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
|
||||
|
@ -44,6 +55,6 @@ export const atoms = {
|
|||
layout,
|
||||
terminal,
|
||||
localData,
|
||||
lightBox,
|
||||
lightBox: lightBox[0],
|
||||
containerQueries,
|
||||
};
|
||||
|
|
|
@ -35,7 +35,6 @@ export const useLocalData = (): void => {
|
|||
const setCurrencies = useAtomSetter(currencies);
|
||||
const setLangui = useAtomSetter(langui);
|
||||
|
||||
const { locale } = useRouter();
|
||||
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
|
||||
const { data: rawCurrencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
|
||||
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
|
||||
|
@ -54,6 +53,6 @@ export const useLocalData = (): void => {
|
|||
|
||||
useEffect(() => {
|
||||
console.log("[useLocalData] Refresh langui");
|
||||
setLangui(processLangui(rawLangui, locale));
|
||||
}, [locale, rawLangui, setLangui]);
|
||||
setLangui(processLangui(rawLangui, "en"));
|
||||
}, [rawLangui, setLangui]);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { useRouter } from "next/router";
|
||||
// import { useRouter } from "next/router";
|
||||
import { useLayoutEffect, useEffect } from "react";
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
|
||||
import { atomPairing, useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
|
||||
import { getDefaultPreferredLanguages } from "helpers/locales";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
import { usePrefersDarkMode } from "hooks/useMediaQuery";
|
||||
import { Ids } from "types/ids";
|
||||
|
||||
export enum ThemeMode {
|
||||
Dark = "dark",
|
||||
|
@ -32,11 +33,10 @@ export const settings = {
|
|||
};
|
||||
|
||||
export const useSettings = (): void => {
|
||||
const router = useRouter();
|
||||
const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
|
||||
// const router = useRouter();
|
||||
// const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
|
||||
const fontSize = useAtomGetter(fontSizeAtom);
|
||||
const isDyslexic = useAtomGetter(dyslexicAtom);
|
||||
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
|
||||
const setDarkMode = useAtomSetter(darkModeAtom);
|
||||
const themeMode = useAtomGetter(themeModeAtom);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -46,19 +46,6 @@ export const useSettings = (): void => {
|
|||
}
|
||||
}, [fontSize]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isDyslexic) {
|
||||
next.classList.add("set-theme-font-dyslexic");
|
||||
next.classList.remove("set-theme-font-standard");
|
||||
} else {
|
||||
next.classList.add("set-theme-font-standard");
|
||||
next.classList.remove("set-theme-font-dyslexic");
|
||||
}
|
||||
}
|
||||
}, [isDyslexic]);
|
||||
|
||||
/* DARK MODE */
|
||||
const prefersDarkMode = usePrefersDarkMode();
|
||||
|
||||
|
@ -66,31 +53,21 @@ export const useSettings = (): void => {
|
|||
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
|
||||
}, [prefersDarkMode, setDarkMode, themeMode]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const next = document.getElementById("__next");
|
||||
if (isDefined(next)) {
|
||||
if (isDarkMode) {
|
||||
next.classList.add("set-theme-dark");
|
||||
next.classList.remove("set-theme-light");
|
||||
} else {
|
||||
next.classList.add("set-theme-light");
|
||||
next.classList.remove("set-theme-dark");
|
||||
}
|
||||
}
|
||||
}, [isDarkMode]);
|
||||
|
||||
/* PREFERRED LANGUAGES */
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
if (preferredLanguages.length === 0) {
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
|
||||
}
|
||||
} else if (router.locale !== preferredLanguages[0]) {
|
||||
*/
|
||||
/*
|
||||
* Using a timeout to the code getting stuck into a loop when reaching the website with a
|
||||
* different preferredLanguages[0] from router.locale
|
||||
*/
|
||||
/*
|
||||
setTimeout(
|
||||
async () =>
|
||||
router.replace(router.asPath, router.asPath, {
|
||||
|
@ -100,4 +77,5 @@ export const useSettings = (): void => {
|
|||
);
|
||||
}
|
||||
}, [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 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];
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
"target": "ES6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"importHelpers": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
@ -17,7 +21,20 @@
|
|||
"incremental": true,
|
||||
"baseUrl": "src"
|
||||
// "noUncheckedIndexedAccess": true
|
||||
,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue