Improved stuff

This commit is contained in:
DrMint 2022-06-18 21:53:23 +02:00
parent 6ae54c39d4
commit bc0764c0d0
40 changed files with 1074 additions and 963 deletions

View File

@ -27,7 +27,7 @@ module.exports = {
}, },
{ {
source: "/gallery", source: "/gallery",
destination: "https://gallery.accords-library.com/", destination: "https://gallery.accords-library.com/posts",
permanent: false, permanent: false,
}, },
]; ];

View File

@ -5,7 +5,13 @@ import { AppStaticProps } from "graphql/getAppStaticProps";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { prettyLanguage, prettySlug } from "helpers/formatters"; import { prettyLanguage, prettySlug } from "helpers/formatters";
import { getOgImage, ImageQuality } from "helpers/img"; import { getOgImage, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import {
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
isUndefined,
iterateMap,
} from "helpers/others";
// import { getClient, Indexes, search, SearchResult } from "helpers/search"; // import { getClient, Indexes, search, SearchResult } from "helpers/search";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
@ -19,6 +25,7 @@ import { ButtonGroup } from "./Inputs/ButtonGroup";
import { OrderableList } from "./Inputs/OrderableList"; import { OrderableList } from "./Inputs/OrderableList";
import { Select } from "./Inputs/Select"; import { Select } from "./Inputs/Select";
import { TextInput } from "./Inputs/TextInput"; import { TextInput } from "./Inputs/TextInput";
import { ContentPlaceholder } from "./PanelComponents/ContentPlaceholder";
import { MainPanel } from "./Panels/MainPanel"; import { MainPanel } from "./Panels/MainPanel";
import { Popup } from "./Popup"; import { Popup } from "./Popup";
@ -98,7 +105,7 @@ export function AppLayout(props: Props): JSX.Element {
if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return; if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
if (mainPanelOpen === true) { if (mainPanelOpen === true) {
setMainPanelOpen(false); setMainPanelOpen(false);
} else if (subPanel === true && contentPanel === true) { } else if (isDefined(subPanel) && isDefined(contentPanel)) {
setSubPanelOpen(true); setSubPanelOpen(true);
} }
} }
@ -116,7 +123,7 @@ export function AppLayout(props: Props): JSX.Element {
}); });
const turnSubIntoContent = useMemo( const turnSubIntoContent = useMemo(
() => isDefined(subPanel) && isDefined(contentPanel), () => isDefined(subPanel) && isUndefined(contentPanel),
[contentPanel, subPanel] [contentPanel, subPanel]
); );
@ -173,11 +180,8 @@ export function AppLayout(props: Props): JSX.Element {
const currencyOptions = useMemo(() => { const currencyOptions = useMemo(() => {
const list: string[] = []; const list: string[] = [];
currencies.map((currentCurrency) => { filterHasAttributes(currencies).map((currentCurrency) => {
if ( if (isDefinedAndNotEmpty(currentCurrency.attributes.code))
currentCurrency.attributes &&
isDefinedAndNotEmpty(currentCurrency.attributes.code)
)
list.push(currentCurrency.attributes.code); list.push(currentCurrency.attributes.code);
}); });
return list; return list;
@ -283,15 +287,10 @@ export function AppLayout(props: Props): JSX.Element {
{isDefined(contentPanel) ? ( {isDefined(contentPanel) ? (
contentPanel contentPanel
) : ( ) : (
<div className="grid h-full place-content-center"> <ContentPlaceholder
<div message={langui.select_option_sidebar ?? ""}
className="grid grid-flow-col place-items-center gap-9 rounded-2xl icon={Icon.ChevronLeft}
border-2 border-dotted border-dark p-8 text-dark opacity-40" />
>
<p className="text-4xl"></p>
<p className="w-64 text-2xl">{langui.select_option_sidebar}</p>
</div>
</div>
)} )}
</div> </div>
@ -299,11 +298,10 @@ export function AppLayout(props: Props): JSX.Element {
{isDefined(subPanel) && ( {isDefined(subPanel) && (
<div <div
className={cJoin( className={cJoin(
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted `texture-paper-dots overflow-y-scroll border-r-[1px] border-dark/50 bg-light
border-black bg-light transition-transform duration-300 [grid-area:sub] transition-transform duration-300 [grid-area:sub] [scrollbar-width:none]
[scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%] mobile:justify-self-end
mobile:justify-self-end mobile:border-r-0 mobile:border-l-[1px] mobile:border-r-0 mobile:border-l-[1px] mobile:[grid-area:content]`,
mobile:[grid-area:content]`,
turnSubIntoContent turnSubIntoContent
? "mobile:w-full mobile:border-l-0" ? "mobile:w-full mobile:border-l-0"
: subPanelOpen === true : subPanelOpen === true
@ -318,10 +316,10 @@ export function AppLayout(props: Props): JSX.Element {
{/* Main panel */} {/* Main panel */}
<div <div
className={cJoin( className={cJoin(
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted `texture-paper-dots overflow-y-scroll border-r-[1px] border-dark/50 bg-light
border-black bg-light transition-transform duration-300 [grid-area:main] transition-transform duration-300 [grid-area:main] [scrollbar-width:none]
[scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%] mobile:justify-self-start
mobile:justify-self-start mobile:[grid-area:content]`, mobile:[grid-area:content]`,
cIf(mainPanelOpen === false, "mobile:-translate-x-full") cIf(mainPanelOpen === false, "mobile:-translate-x-full")
)} )}
> >
@ -399,8 +397,9 @@ export function AppLayout(props: Props): JSX.Element {
]) ])
} }
onChange={(items) => { onChange={(items) => {
const newPreferredLanguages = [...items].map( const newPreferredLanguages = iterateMap(
([code]) => code items,
(code) => code
); );
setPreferredLanguages(newPreferredLanguages); setPreferredLanguages(newPreferredLanguages);
if (router.locale !== newPreferredLanguages[0]) { if (router.locale !== newPreferredLanguages[0]) {

View File

@ -63,9 +63,8 @@ export function Button(props: Props): JSX.Element {
text-dark transition-all`, text-dark transition-all`,
cIf( cIf(
active, active,
"!border-black bg-black text-light drop-shadow-black-lg", "!border-black bg-black !text-light drop-shadow-black-lg",
`cursor-pointer hover:bg-dark hover:text-light hover:drop-shadow-shade-lg "cursor-pointer hover:bg-dark hover:text-light hover:drop-shadow-shade-lg"
active:border-black active:bg-black active:text-light active:drop-shadow-black-lg`
), ),
className className
)} )}

View File

@ -2,6 +2,7 @@ import { Icon } from "components/Ico";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { prettyLanguage } from "helpers/formatters"; import { prettyLanguage } from "helpers/formatters";
import { iterateMap } from "helpers/others";
import { Fragment } from "react"; import { Fragment } from "react";
import { ToolTip } from "../ToolTip"; import { ToolTip } from "../ToolTip";
@ -22,15 +23,13 @@ export function LanguageSwitcher(props: Props): JSX.Element {
<ToolTip <ToolTip
content={ content={
<div className={cJoin("flex flex-col gap-2", className)}> <div className={cJoin("flex flex-col gap-2", className)}>
{[...locales].map(([locale, value], index) => ( {iterateMap(locales, (locale, value, index) => (
<Fragment key={index}> <Fragment key={index}>
{locale && ( <Button
<Button active={value === localesIndex}
active={value === localesIndex} onClick={() => onLanguageChanged(value)}
onClick={() => onLanguageChanged(value)} text={prettyLanguage(locale, props.languages)}
text={prettyLanguage(locale, props.languages)} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>

View File

@ -1,5 +1,5 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { arrayMove, isDefinedAndNotEmpty } from "helpers/others"; import { isDefinedAndNotEmpty, iterateMap, mapMoveEntry } from "helpers/others";
import { Fragment, useCallback, useState } from "react"; import { Fragment, useCallback, useState } from "react";
@ -16,17 +16,16 @@ export function OrderableList(props: Props): JSX.Element {
const updateOrder = useCallback( const updateOrder = useCallback(
(sourceIndex: number, targetIndex: number) => { (sourceIndex: number, targetIndex: number) => {
const newItems = arrayMove([...items], sourceIndex, targetIndex); const newItems = mapMoveEntry(items, sourceIndex, targetIndex);
const map = new Map(newItems); setItems(newItems);
setItems(map); onChange?.(newItems);
onChange?.(map);
}, },
[items, onChange] [items, onChange]
); );
return ( return (
<div className="grid gap-2"> <div className="grid gap-2">
{[...items].map(([key, value], index) => ( {iterateMap(items, (key, value, index) => (
<Fragment key={key}> <Fragment key={key}>
{props.insertLabels && {props.insertLabels &&
isDefinedAndNotEmpty(props.insertLabels.get(index)) && ( isDefinedAndNotEmpty(props.insertLabels.get(index)) && (

View File

@ -43,7 +43,7 @@ export function Select(props: Props): JSX.Element {
className="!text-xs" className="!text-xs"
onClick={() => { onClick={() => {
setState(-1); setState(-1);
toggleOpened(); setOpened(false);
}} }}
/> />
)} )}

View File

@ -11,7 +11,7 @@ interface Props {
} }
export function Switch(props: Props): JSX.Element { export function Switch(props: Props): JSX.Element {
const { state, setState, className, disabled } = props; const { state, setState, className, disabled = false } = props;
const toggleState = useToggle(setState); const toggleState = useToggle(setState);
return ( return (
<div <div
@ -26,7 +26,7 @@ export function Switch(props: Props): JSX.Element {
className className
)} )}
onClick={() => { onClick={() => {
if (disabled === false) toggleState(); if (!disabled) toggleState();
}} }}
> >
<div <div

View File

@ -3,7 +3,9 @@ import { Ico, Icon } from "components/Ico";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { GetLibraryItemQuery } from "graphql/generated"; import { GetLibraryItemQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cIf, cJoin } from "helpers/className";
import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { filterHasAttributes } from "helpers/others";
import { useToggle } from "hooks/useToggle"; import { useToggle } from "hooks/useToggle";
import { useState } from "react"; import { useState } from "react";
@ -29,9 +31,10 @@ export function ContentLine(props: Props): JSX.Element {
if (content.attributes) { if (content.attributes) {
return ( return (
<div <div
className={`grid gap-2 rounded-lg px-4 ${ className={cJoin(
opened && "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade" "grid gap-2 rounded-lg px-4",
}`} cIf(opened, "my-2 h-auto bg-mid py-3 shadow-inner-sm shadow-shade")
)}
> >
<div <div
className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center className="grid grid-cols-[auto_auto_1fr_auto_12ch] place-items-center
@ -52,11 +55,11 @@ export function ContentLine(props: Props): JSX.Element {
</h3> </h3>
</a> </a>
<div className="flex flex-row flex-wrap gap-1"> <div className="flex flex-row flex-wrap gap-1">
{content.attributes.content?.data?.attributes?.categories?.data.map( {filterHasAttributes(
(category) => ( content.attributes.content?.data?.attributes?.categories?.data
<Chip key={category.id}>{category.attributes?.short}</Chip> ).map((category) => (
) <Chip key={category.id}>{category.attributes.short}</Chip>
)} ))}
</div> </div>
<p className="h-4 w-full border-b-2 border-dotted border-black opacity-30"></p> <p className="h-4 w-full border-b-2 border-dotted border-black opacity-30"></p>
<p> <p>

View File

@ -7,10 +7,15 @@ import { GetLibraryItemScansQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img"; import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img";
import { isInteger } from "helpers/numbers"; import { isInteger } from "helpers/numbers";
import { getStatusDescription, isDefinedAndNotEmpty } from "helpers/others"; import {
filterHasAttributes,
getStatusDescription,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
interface Props { interface Props {
openLightBox: (images: string[], index?: number) => void; openLightBox: (images: string[], index?: number) => void;
@ -81,9 +86,14 @@ export function ScanSet(props: Props): JSX.Element {
}, },
}); });
const pages = useMemo(
() => selectedScan && filterHasAttributes(selectedScan.pages?.data),
[selectedScan]
);
return ( return (
<> <>
{selectedScan && ( {selectedScan && isDefined(pages) && (
<div> <div>
<div <div
className="flex flex-row flex-wrap place-items-center className="flex flex-row flex-wrap place-items-center
@ -126,16 +136,16 @@ export function ScanSet(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Scanners"}:</p> <p className="font-headers">{"Scanners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.scanners.data.map((scanner) => ( {filterHasAttributes(selectedScan.scanners.data).map(
<Fragment key={scanner.id}> (scanner) => (
{scanner.attributes && ( <Fragment key={scanner.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={scanner.attributes} recorder={scanner.attributes}
/> />
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
</div> </div>
)} )}
@ -144,16 +154,18 @@ export function ScanSet(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Cleaners"}:</p> <p className="font-headers">{"Cleaners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.cleaners.data.map((cleaner) => ( {filterHasAttributes(selectedScan.cleaners.data).map(
<Fragment key={cleaner.id}> (cleaner) => (
{cleaner.attributes && ( <Fragment key={cleaner.id}>
<RecorderChip {cleaner.attributes && (
langui={langui} <RecorderChip
recorder={cleaner.attributes} langui={langui}
/> recorder={cleaner.attributes}
)} />
</Fragment> )}
))} </Fragment>
)
)}
</div> </div>
</div> </div>
)} )}
@ -163,16 +175,18 @@ export function ScanSet(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Typesetters"}:</p> <p className="font-headers">{"Typesetters"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.typesetters.data.map((typesetter) => ( {filterHasAttributes(selectedScan.typesetters.data).map(
<Fragment key={typesetter.id}> (typesetter) => (
{typesetter.attributes && ( <Fragment key={typesetter.id}>
<RecorderChip {typesetter.attributes && (
langui={langui} <RecorderChip
recorder={typesetter.attributes} langui={langui}
/> recorder={typesetter.attributes}
)} />
</Fragment> )}
))} </Fragment>
)
)}
</div> </div>
</div> </div>
)} )}
@ -188,18 +202,15 @@ export function ScanSet(props: Props): JSX.Element {
className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] mobile:grid-cols-2" desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))] mobile:grid-cols-2"
> >
{selectedScan.pages?.data.map((page, index) => ( {pages.map((page, index) => (
<div <div
key={page.id} key={page.id}
className="cursor-pointer transition-transform className="cursor-pointer transition-transform
drop-shadow-shade-lg hover:scale-[1.02]" drop-shadow-shade-lg hover:scale-[1.02]"
onClick={() => { onClick={() => {
const images: string[] = []; const images: string[] = [];
selectedScan.pages?.data.map((image) => { pages.map((image) => {
if ( if (isDefinedAndNotEmpty(image.attributes.url))
image.attributes &&
isDefinedAndNotEmpty(image.attributes.url)
)
images.push( images.push(
getAssetURL(image.attributes.url, ImageQuality.Large) getAssetURL(image.attributes.url, ImageQuality.Large)
); );

View File

@ -8,7 +8,7 @@ import {
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { getStatusDescription } from "helpers/others"; import { filterHasAttributes, getStatusDescription } from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment } from "react"; import { Fragment } from "react";
@ -87,16 +87,16 @@ export function ScanSetCover(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Scanners"}:</p> <p className="font-headers">{"Scanners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.scanners.data.map((scanner) => ( {filterHasAttributes(selectedScan.scanners.data).map(
<Fragment key={scanner.id}> (scanner) => (
{scanner.attributes && ( <Fragment key={scanner.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={scanner.attributes} recorder={scanner.attributes}
/> />
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
</div> </div>
)} )}
@ -105,16 +105,16 @@ export function ScanSetCover(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Cleaners"}:</p> <p className="font-headers">{"Cleaners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.cleaners.data.map((cleaner) => ( {filterHasAttributes(selectedScan.cleaners.data).map(
<Fragment key={cleaner.id}> (cleaner) => (
{cleaner.attributes && ( <Fragment key={cleaner.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={cleaner.attributes} recorder={cleaner.attributes}
/> />
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
</div> </div>
)} )}
@ -124,16 +124,16 @@ export function ScanSetCover(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Typesetters"}:</p> <p className="font-headers">{"Typesetters"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedScan.typesetters.data.map((typesetter) => ( {filterHasAttributes(selectedScan.typesetters.data).map(
<Fragment key={typesetter.id}> (typesetter) => (
{typesetter.attributes && ( <Fragment key={typesetter.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={typesetter.attributes} recorder={typesetter.attributes}
/> />
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
</div> </div>
)} )}

View File

@ -0,0 +1,30 @@
import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className";
import { isDefined } from "helpers/others";
interface Props {
message: string;
icon?: Icon;
}
export function ContentPlaceholder(props: Props): JSX.Element {
const { message, icon } = props;
return (
<div className="grid h-full 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"
>
{isDefined(icon) && <Ico icon={icon} className="!text-[300%]" />}
<p
className={cJoin(
"w-64 text-2xl",
cIf(!isDefined(icon), "text-center")
)}
>
{message}
</p>
</div>
</div>
);
}

View File

@ -17,7 +17,15 @@ interface Props {
} }
export function NavOption(props: Props): JSX.Element { export function NavOption(props: Props): JSX.Element {
const { url, icon, title, subtitle, border, reduced, onClick } = props; const {
url,
icon,
title,
subtitle,
border = false,
reduced = false,
onClick,
} = props;
const router = useRouter(); const router = useRouter();
const isActive = useMemo( const isActive = useMemo(
() => router.asPath.startsWith(url), () => router.asPath.startsWith(url),
@ -36,7 +44,7 @@ export function NavOption(props: Props): JSX.Element {
} }
placement="right" placement="right"
className="text-left" className="text-left"
disabled={reduced === false} disabled={!reduced}
> >
<div <div
onClick={(event) => { onClick={(event) => {
@ -63,7 +71,7 @@ export function NavOption(props: Props): JSX.Element {
> >
{icon && <Ico icon={icon} className="mt-[-.1em] !text-2xl" />} {icon && <Ico icon={icon} className="mt-[-.1em] !text-2xl" />}
{reduced === false && ( {!reduced && (
<div> <div>
<h3 className="text-2xl">{title}</h3> <h3 className="text-2xl">{title}</h3>
{isDefinedAndNotEmpty(subtitle) && ( {isDefinedAndNotEmpty(subtitle) && (

View File

@ -15,10 +15,10 @@ export function ContentPanel(props: Props): JSX.Element {
const { width = ContentPanelWidthSizes.Default, children } = props; const { width = ContentPanelWidthSizes.Default, children } = props;
return ( return (
<div className={`grid px-4 pt-10 pb-20 desktop:py-20 desktop:px-10`}> <div className={`grid h-full px-4 desktop:px-10`}>
<main <main
className={cJoin( className={cJoin(
"place-self-center", "justify-self-center pt-10 pb-20 desktop:pt-20 desktop:pb-32",
width === ContentPanelWidthSizes.Default width === ContentPanelWidthSizes.Default
? "max-w-2xl" ? "max-w-2xl"
: width === ContentPanelWidthSizes.Large : width === ContentPanelWidthSizes.Large

View File

@ -60,7 +60,7 @@ export function MainPanel(props: Props): JSX.Element {
</Link> </Link>
{(!mainPanelReduced || !isDesktop) && ( {(!mainPanelReduced || !isDesktop) && (
<h2 className="text-3xl">Accord&rsquo;s Library</h2> <h2 className="mb-4 text-3xl">Accord&rsquo;s Library</h2>
)} )}
<div <div
@ -155,7 +155,7 @@ export function MainPanel(props: Props): JSX.Element {
*/} */}
<NavOption <NavOption
url="https://gallery.accords-library.com/" url="https://gallery.accords-library.com/posts/"
icon={Icon.Collections} icon={Icon.Collections}
title={langui.gallery} title={langui.gallery}
reduced={mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}

View File

@ -28,7 +28,7 @@ export function Popup(props: Props): JSX.Element {
const { setMenuGestures } = useAppLayout(); const { setMenuGestures } = useAppLayout();
useEffect(() => { useEffect(() => {
setMenuGestures(state); setMenuGestures(!state);
}, [setMenuGestures, state]); }, [setMenuGestures, state]);
return ( return (

View File

@ -1,7 +1,7 @@
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getDescription } from "helpers/description"; import { getDescription } from "helpers/description";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { getStatusDescription } from "helpers/others"; import { filterHasAttributes, getStatusDescription } from "helpers/others";
import { PostWithTranslations } from "helpers/types"; import { PostWithTranslations } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment, useMemo } from "react"; import { Fragment, useMemo } from "react";
@ -102,14 +102,12 @@ export function PostPage(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{"Authors"}:</p> <p className="font-headers">{"Authors"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{post.authors.data.map((author) => ( {filterHasAttributes(post.authors.data).map((author) => (
<Fragment key={author.id}> <Fragment key={author.id}>
{author.attributes && ( <RecorderChip
<RecorderChip langui={langui}
langui={langui} recorder={author.attributes}
recorder={author.attributes} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>

View File

@ -244,7 +244,8 @@ export function PreviewCard(props: Props): JSX.Element {
cIf( cIf(
!keepInfoVisible, !keepInfoVisible,
`-inset-x-0.5 bottom-2 opacity-0 group-hover:opacity-100 hoverable:absolute `-inset-x-0.5 bottom-2 opacity-0 group-hover:opacity-100 hoverable:absolute
hoverable:drop-shadow-shade-lg notHoverable:rounded-b-md` hoverable:drop-shadow-shade-lg notHoverable:opacity-100
notHoverable:rounded-b-md`
) )
)} )}
> >
@ -302,9 +303,7 @@ interface TranslatedProps
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
} }
export function TranslatedPreviewCard( export function TranslatedPreviewCard(props: TranslatedProps): JSX.Element {
props: Immutable<TranslatedProps>
): JSX.Element {
const { const {
translations = [{ title: props.slug, language: "default" }], translations = [{ title: props.slug, language: "default" }],
slug, slug,

View File

@ -89,7 +89,7 @@ interface TranslatedProps
} }
export function TranslatedPreviewLine( export function TranslatedPreviewLine(
props: Immutable<TranslatedProps> props: TranslatedProps
): JSX.Element { ): JSX.Element {
const { const {
translations = [{ title: props.slug, language: "default" }], translations = [{ title: props.slug, language: "default" }],

View File

@ -2,6 +2,7 @@ import { Chip } from "components/Chip";
import { RecorderChipFragment } from "graphql/generated"; import { RecorderChipFragment } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { ImageQuality } from "helpers/img"; import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { Fragment } from "react"; import { Fragment } from "react";
import { Img } from "./Img"; import { Img } from "./Img";
@ -33,13 +34,13 @@ export function RecorderChip(props: Props): JSX.Element {
{recorder.languages?.data && recorder.languages.data.length > 0 && ( {recorder.languages?.data && recorder.languages.data.length > 0 && (
<div className="flex flex-row flex-wrap gap-1"> <div className="flex flex-row flex-wrap gap-1">
<p>{langui.languages}:</p> <p>{langui.languages}:</p>
{recorder.languages.data.map((language) => ( {filterHasAttributes(recorder.languages.data).map(
<Fragment key={language.attributes?.code}> (language) => (
{language.attributes && ( <Fragment key={language.attributes.code}>
<Chip>{language.attributes.code.toUpperCase()}</Chip> <Chip>{language.attributes.code.toUpperCase()}</Chip>
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
)} )}
{recorder.pronouns && ( {recorder.pronouns && (

View File

@ -6,6 +6,7 @@ import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettySlug, slugify } from "helpers/formatters"; import { prettyinlineTitle, prettySlug, slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
@ -89,9 +90,11 @@ export function ThumbnailHeader(props: Props): JSX.Element {
<div className="flex flex-col place-items-center gap-2"> <div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.categories}</h3> <h3 className="text-xl">{langui.categories}</h3>
<div className="flex flex-row flex-wrap place-content-center gap-2"> <div className="flex flex-row flex-wrap place-content-center gap-2">
{categories.data.map((category) => ( {filterHasAttributes(categories.data).map(
<Chip key={category.id}>{category.attributes?.name}</Chip> (category) => (
))} <Chip key={category.id}>{category.attributes.name}</Chip>
)
)}
</div> </div>
</div> </div>
)} )}

View File

@ -6,7 +6,11 @@ import {
GetChronologyItemsQuery, GetChronologyItemsQuery,
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getStatusDescription } from "helpers/others"; import {
filterDefined,
filterHasAttributes,
getStatusDescription,
} from "helpers/others";
import { Fragment } from "react"; import { Fragment } from "react";
@ -44,13 +48,16 @@ export function ChronologyItemComponent(props: Props): JSX.Element {
</p> </p>
<div className="col-start-2 row-span-2 row-start-1 grid gap-4"> <div className="col-start-2 row-span-2 row-start-1 grid gap-4">
{props.item.attributes.events?.map((event) => ( {props.item.attributes.events &&
<Fragment key={event?.id}> filterHasAttributes(props.item.attributes.events, [
{event && ( "id",
"translations",
]).map((event) => (
<Fragment key={event.id}>
<div className="m-0"> <div className="m-0">
{event.translations?.map((translation, translationIndex) => ( {filterDefined(event.translations).map(
<Fragment key={translationIndex}> (translation, translationIndex) => (
{translation && ( <Fragment key={translationIndex}>
<Fragment> <Fragment>
<div <div
className="grid className="grid
@ -94,9 +101,9 @@ export function ChronologyItemComponent(props: Props): JSX.Element {
"" ""
)} )}
</Fragment> </Fragment>
)} </Fragment>
</Fragment> )
))} )}
<p className="mt-1 grid grid-flow-col gap-1 place-self-start text-xs text-dark"> <p className="mt-1 grid grid-flow-col gap-1 place-self-start text-xs text-dark">
{event.source?.data ? ( {event.source?.data ? (
@ -109,9 +116,8 @@ export function ChronologyItemComponent(props: Props): JSX.Element {
)} )}
</p> </p>
</div> </div>
)} </Fragment>
</Fragment> ))}
))}
</div> </div>
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettyDate } from "./formatters"; import { prettyinlineTitle, prettyDate } from "./formatters";
import { convertPrice } from "./numbers"; import { convertPrice } from "./numbers";
import { isDefined } from "./others"; import { isDefined, mapRemoveEmptyValues } from "./others";
import { LibraryItemUserStatus } from "./types"; import { LibraryItemUserStatus } from "./types";
type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
type GroupLibraryItems = Map<string, Items>; type GroupLibraryItems = Map<string, Items>;
@ -13,67 +13,67 @@ export function getGroups(
groupByType: number, groupByType: number,
items: Items items: Items
): GroupLibraryItems { ): GroupLibraryItems {
const groups: GroupLibraryItems = new Map();
switch (groupByType) { switch (groupByType) {
case 0: { case 0: {
const typeGroup = new Map(); const noCategory = langui.no_category ?? "No category";
typeGroup.set("Drakengard 1", []); groups.set("Drakengard 1", []);
typeGroup.set("Drakengard 1.3", []); groups.set("Drakengard 1.3", []);
typeGroup.set("Drakengard 2", []); groups.set("Drakengard 2", []);
typeGroup.set("Drakengard 3", []); groups.set("Drakengard 3", []);
typeGroup.set("Drakengard 4", []); groups.set("Drakengard 4", []);
typeGroup.set("NieR Gestalt", []); groups.set("NieR Gestalt", []);
typeGroup.set("NieR Replicant", []); groups.set("NieR Replicant", []);
typeGroup.set("NieR Replicant ver.1.22474487139...", []); groups.set("NieR Replicant ver.1.22474487139...", []);
typeGroup.set("NieR:Automata", []); groups.set("NieR:Automata", []);
typeGroup.set("NieR Re[in]carnation", []); groups.set("NieR Re[in]carnation", []);
typeGroup.set("SINoALICE", []); groups.set("SINoALICE", []);
typeGroup.set("Voice of Cards", []); groups.set("Voice of Cards", []);
typeGroup.set("Final Fantasy XIV", []); groups.set("Final Fantasy XIV", []);
typeGroup.set("Thou Shalt Not Die", []); groups.set("Thou Shalt Not Die", []);
typeGroup.set("Bakuken", []); groups.set("Bakuken", []);
typeGroup.set("YoRHa", []); groups.set("YoRHa", []);
typeGroup.set("YoRHa Boys", []); groups.set("YoRHa Boys", []);
typeGroup.set(langui.no_category, []); groups.set(noCategory, []);
items.map((item) => { items.map((item) => {
if (item.attributes?.categories?.data.length === 0) { if (item.attributes?.categories?.data.length === 0) {
typeGroup.get(langui.no_category)?.push(item); groups.get(noCategory)?.push(item);
} else { } else {
item.attributes?.categories?.data.map((category) => { item.attributes?.categories?.data.map((category) => {
typeGroup.get(category.attributes?.name)?.push(item); groups.get(category.attributes?.name ?? noCategory)?.push(item);
}); });
} }
}); });
break;
return typeGroup;
} }
case 1: { case 1: {
const group = new Map(); groups.set(langui.audio ?? "Audio", []);
group.set(langui.audio ?? "Audio", []); groups.set(langui.game ?? "Game", []);
group.set(langui.game ?? "Game", []); groups.set(langui.textual ?? "Textual", []);
group.set(langui.textual ?? "Textual", []); groups.set(langui.video ?? "Video", []);
group.set(langui.video ?? "Video", []); groups.set(langui.other ?? "Other", []);
group.set(langui.other ?? "Other", []); groups.set(langui.group ?? "Group", []);
group.set(langui.group ?? "Group", []); groups.set(langui.no_type ?? "No type", []);
group.set(langui.no_type ?? "No type", []);
items.map((item) => { items.map((item) => {
if (item.attributes?.metadata && item.attributes.metadata.length > 0) { if (item.attributes?.metadata && item.attributes.metadata.length > 0) {
switch (item.attributes.metadata[0]?.__typename) { switch (item.attributes.metadata[0]?.__typename) {
case "ComponentMetadataAudio": case "ComponentMetadataAudio":
group.get(langui.audio ?? "Audio")?.push(item); groups.get(langui.audio ?? "Audio")?.push(item);
break; break;
case "ComponentMetadataGame": case "ComponentMetadataGame":
group.get(langui.game ?? "Game")?.push(item); groups.get(langui.game ?? "Game")?.push(item);
break; break;
case "ComponentMetadataBooks": case "ComponentMetadataBooks":
group.get(langui.textual ?? "Textual")?.push(item); groups.get(langui.textual ?? "Textual")?.push(item);
break; break;
case "ComponentMetadataVideo": case "ComponentMetadataVideo":
group.get(langui.video ?? "Video")?.push(item); groups.get(langui.video ?? "Video")?.push(item);
break; break;
case "ComponentMetadataOther": case "ComponentMetadataOther":
group.get(langui.other ?? "Other")?.push(item); groups.get(langui.other ?? "Other")?.push(item);
break; break;
case "ComponentMetadataGroup": case "ComponentMetadataGroup":
switch ( switch (
@ -81,19 +81,19 @@ export function getGroups(
?.slug ?.slug
) { ) {
case "audio": case "audio":
group.get(langui.audio ?? "Audio")?.push(item); groups.get(langui.audio ?? "Audio")?.push(item);
break; break;
case "video": case "video":
group.get(langui.video ?? "Video")?.push(item); groups.get(langui.video ?? "Video")?.push(item);
break; break;
case "game": case "game":
group.get(langui.game ?? "Game")?.push(item); groups.get(langui.game ?? "Game")?.push(item);
break; break;
case "textual": case "textual":
group.get(langui.textual ?? "Textual")?.push(item); groups.get(langui.textual ?? "Textual")?.push(item);
break; break;
case "mixed": case "mixed":
group.get(langui.group ?? "Group")?.push(item); groups.get(langui.group ?? "Group")?.push(item);
break; break;
default: { default: {
throw new Error( throw new Error(
@ -107,10 +107,10 @@ export function getGroups(
} }
} }
} else { } else {
group.get(langui.no_type ?? "No type")?.push(item); groups.get(langui.no_type ?? "No type")?.push(item);
} }
}); });
return group; break;
} }
case 2: { case 2: {
@ -121,29 +121,28 @@ export function getGroups(
years.push(item.attributes.release_date.year); years.push(item.attributes.release_date.year);
} }
}); });
const group = new Map();
years.sort((a, b) => a - b); years.sort((a, b) => a - b);
years.map((year) => { years.map((year) => {
group.set(year.toString(), []); groups.set(year.toString(), []);
}); });
group.set(langui.no_year ?? "No year", []); groups.set(langui.no_year ?? "No year", []);
items.map((item) => { items.map((item) => {
if (item.attributes?.release_date?.year) { if (item.attributes?.release_date?.year) {
group.get(item.attributes.release_date.year.toString())?.push(item); groups.get(item.attributes.release_date.year.toString())?.push(item);
} else { } else {
group.get(langui.no_year ?? "No year")?.push(item); groups.get(langui.no_year ?? "No year")?.push(item);
} }
}); });
break;
return group;
} }
default: { default: {
const group = new Map(); groups.set("", items);
group.set("", items); break;
return group;
} }
} }
return mapRemoveEmptyValues(groups);
} }
export function filterItems( export function filterItems(
@ -155,7 +154,7 @@ export function filterItems(
showSecondaryItems: boolean, showSecondaryItems: boolean,
filterUserStatus: LibraryItemUserStatus | undefined filterUserStatus: LibraryItemUserStatus | undefined
): Items { ): Items {
return [...items].filter((item) => { return items.filter((item) => {
if (!showSubitems && !item.attributes?.root_item) return false; if (!showSubitems && !item.attributes?.root_item) return false;
if (showSubitems && isUntangibleGroupItem(item.attributes?.metadata?.[0])) { if (showSubitems && isUntangibleGroupItem(item.attributes?.metadata?.[0])) {
return false; return false;
@ -212,7 +211,7 @@ export function sortBy(
): Items { ): Items {
switch (orderByType) { switch (orderByType) {
case 0: case 0:
return [...items].sort((a, b) => { return items.sort((a, b) => {
const titleA = prettyinlineTitle( const titleA = prettyinlineTitle(
"", "",
a.attributes?.title, a.attributes?.title,
@ -226,7 +225,7 @@ export function sortBy(
return titleA.localeCompare(titleB); return titleA.localeCompare(titleB);
}); });
case 1: case 1:
return [...items].sort((a, b) => { return items.sort((a, b) => {
const priceA = a.attributes?.price const priceA = a.attributes?.price
? convertPrice(a.attributes.price, currencies[0]) ? convertPrice(a.attributes.price, currencies[0])
: 99999; : 99999;
@ -236,7 +235,7 @@ export function sortBy(
return priceA - priceB; return priceA - priceB;
}); });
case 2: case 2:
return [...items].sort((a, b) => { return items.sort((a, b) => {
const dateA = a.attributes?.release_date const dateA = a.attributes?.release_date
? prettyDate(a.attributes.release_date) ? prettyDate(a.attributes.release_date)
: "9999"; : "9999";

View File

@ -4,6 +4,7 @@ import {
GetLibraryItemScansQuery, GetLibraryItemScansQuery,
} from "graphql/generated"; } from "graphql/generated";
import { AppStaticProps } from "../graphql/getAppStaticProps"; import { AppStaticProps } from "../graphql/getAppStaticProps";
import { SelectiveRequiredNonNullable } from "./types";
type SortContentProps = type SortContentProps =
| NonNullable< | NonNullable<
@ -59,11 +60,6 @@ export function getStatusDescription(
} }
} }
export function arrayMove<T>(arr: T[], old_index: number, new_index: number) {
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}
export function isDefined<T>(t: T): t is NonNullable<T> { export function isDefined<T>(t: T): t is NonNullable<T> {
return t !== null && t !== undefined; return t !== null && t !== undefined;
} }
@ -78,6 +74,51 @@ export function isDefinedAndNotEmpty(
return isDefined(string) && string.length > 0; return isDefined(string) && string.length > 0;
} }
export function filterDefined<T>(t: T[]): NonNullable<T>[] { export function filterDefined<T>(t: T[] | undefined | null): NonNullable<T>[] {
if (isUndefined(t)) return [];
return t.filter((item) => isDefined(item)) as NonNullable<T>[]; return t.filter((item) => isDefined(item)) as NonNullable<T>[];
} }
export function filterHasAttributes<T, P extends keyof NonNullable<T>>(
t: T[] | undefined | null,
attributes?: P[]
): SelectiveRequiredNonNullable<NonNullable<T>, P>[] {
if (isUndefined(t)) return [];
return t.filter((item) => {
if (isDefined(item)) {
const attributesToCheck = attributes ?? (Object.keys(item) as P[]);
return attributesToCheck.every((attribute) => isDefined(item[attribute]));
}
return false;
}) as unknown as SelectiveRequiredNonNullable<NonNullable<T>, P>[];
}
export function iterateMap<K, V, U>(
map: Map<K, V>,
callbackfn: (key: K, value: V, index: number) => U
): U[] {
const result: U[] = [];
let index = 0;
for (const [key, value] of map.entries()) {
result.push(callbackfn(key, value, index));
index += 1;
}
return result;
}
export function mapMoveEntry<K, V>(
map: Map<K, V>,
sourceIndex: number,
targetIndex: number
) {
return new Map(arrayMove([...map], sourceIndex, targetIndex));
}
function arrayMove<T>(arr: T[], sourceIndex: number, targetIndex: number) {
arr.splice(targetIndex, 0, arr.splice(sourceIndex, 1)[0]);
return arr;
}
export function mapRemoveEmptyValues<K, V>(groups: Map<K, V[]>): Map<K, V[]> {
return new Map([...groups].filter(([_, items]) => items.length > 0));
}

View File

@ -30,9 +30,13 @@ export interface WikiPageWithTranslations
translations: NonNullable<WikiPage["translations"]>; translations: NonNullable<WikiPage["translations"]>;
} }
export type RequiredNonNullable<T> = Required<{ export type RequiredNonNullable<T> = {
[P in keyof T]: NonNullable<T[P]>; [P in keyof T]-?: NonNullable<T[P]>;
}>; };
export type SelectiveRequiredNonNullable<T, K extends keyof T> = Omit<T, K> & {
[P in K]-?: NonNullable<T[P]>;
};
export enum LibraryItemUserStatus { export enum LibraryItemUserStatus {
None = 0, None = 0,

View File

@ -1,7 +1,7 @@
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher"; import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { isDefined } from "helpers/others"; import { filterDefined, isDefined } from "helpers/others";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -39,11 +39,9 @@ export function useSmartLanguage<T>(
const availableLocales = useMemo(() => { const availableLocales = useMemo(() => {
const map = new Map<string, number>(); const map = new Map<string, number>();
items.map((elem, index) => { filterDefined(items).map((elem, index) => {
if (isDefined(elem)) { const result = languageExtractor(elem);
const result = languageExtractor(elem); if (isDefined(result)) map.set(result, index);
if (isDefined(result)) map.set(result, index);
}
}); });
return map; return map;
}, [items, languageExtractor]); }, [items, languageExtractor]);

View File

@ -24,7 +24,7 @@ import { Fragment, useState } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
channel: NonNullable< channel: NonNullable<
@ -74,27 +74,25 @@ export default function Channel(props: Props): JSX.Element {
className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0 className="grid items-start gap-8 border-b-[3px] border-dotted pb-12 last-of-type:border-0
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2" desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2"
> >
{channel?.videos?.data.map((video) => ( {filterHasAttributes(channel?.videos?.data).map((video) => (
<Fragment key={video.id}> <Fragment key={video.id}>
{video.attributes && ( <PreviewCard
<PreviewCard href={`/archives/videos/v/${video.attributes.uid}`}
href={`/archives/videos/v/${video.attributes.uid}`} title={video.attributes.title}
title={video.attributes.title} thumbnail={getVideoThumbnailURL(video.attributes.uid)}
thumbnail={getVideoThumbnailURL(video.attributes.uid)} thumbnailAspectRatio="16/9"
thumbnailAspectRatio="16/9" keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ release_date: video.attributes.published_date,
release_date: video.attributes.published_date, views: video.attributes.views,
views: video.attributes.views, author: channel?.title,
author: channel.title, position: "Top",
position: "Top", }}
}} hoverlay={{
hoverlay={{ __typename: "Video",
__typename: "Video", duration: video.attributes.duration,
duration: video.attributes.duration, }}
}} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>
@ -137,13 +135,12 @@ export async function getStaticPaths(
const channels = await sdk.getVideoChannelsSlugs(); const channels = await sdk.getVideoChannelsSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
if (channels.videoChannels?.data) if (channels.videoChannels?.data)
channels.videoChannels.data.map((channel) => { filterHasAttributes(channels.videoChannels.data).map((channel) => {
context.locales?.map((local) => { context.locales?.map((local) => {
if (channel.attributes) paths.push({
paths.push({ params: { uid: channel.attributes.uid },
params: { uid: channel.attributes.uid }, locale: local,
locale: local, });
});
}); });
}); });
return { return {

View File

@ -18,6 +18,7 @@ import { GetVideosPreviewQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate } from "helpers/formatters"; import { prettyDate } from "helpers/formatters";
import { filterHasAttributes } from "helpers/others";
import { getVideoThumbnailURL } from "helpers/videos"; import { getVideoThumbnailURL } from "helpers/videos";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
@ -95,27 +96,25 @@ export default function Videos(props: Props): JSX.Element {
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
thin:grid-cols-1" thin:grid-cols-1"
> >
{paginatedVideos[page].map((video) => ( {filterHasAttributes(paginatedVideos[page]).map((video) => (
<Fragment key={video.id}> <Fragment key={video.id}>
{video.attributes && ( <PreviewCard
<PreviewCard href={`/archives/videos/v/${video.attributes.uid}`}
href={`/archives/videos/v/${video.attributes.uid}`} title={video.attributes.title}
title={video.attributes.title} thumbnail={getVideoThumbnailURL(video.attributes.uid)}
thumbnail={getVideoThumbnailURL(video.attributes.uid)} thumbnailAspectRatio="16/9"
thumbnailAspectRatio="16/9" keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ release_date: video.attributes.published_date,
release_date: video.attributes.published_date, views: video.attributes.views,
views: video.attributes.views, author: video.attributes.channel?.data?.attributes?.title,
author: video.attributes.channel?.data?.attributes?.title, position: "Top",
position: "Top", }}
}} hoverlay={{
hoverlay={{ __typename: "Video",
__typename: "Video", duration: video.attributes.duration,
duration: video.attributes.duration, }}
}} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>

View File

@ -18,7 +18,7 @@ import { GetVideoQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters"; import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
import { getVideoFile } from "helpers/videos"; import { getVideoFile } from "helpers/videos";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { import {
@ -213,10 +213,9 @@ export async function getStaticPaths(
const videos = await sdk.getVideosSlugs(); const videos = await sdk.getVideosSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
if (videos.videos?.data) if (videos.videos?.data)
videos.videos.data.map((video) => { filterHasAttributes(videos.videos.data).map((video) => {
context.locales?.map((local) => { context.locales?.map((local) => {
if (video.attributes) paths.push({ params: { uid: video.attributes.uid }, locale: local });
paths.push({ params: { uid: video.attributes.uid }, locale: local });
}); });
}); });
return { return {

View File

@ -26,7 +26,11 @@ import {
prettySlug, prettySlug,
} from "helpers/formatters"; } from "helpers/formatters";
import { isUntangibleGroupItem } from "helpers/libraryItem"; import { isUntangibleGroupItem } from "helpers/libraryItem";
import { getStatusDescription } from "helpers/others"; import {
filterHasAttributes,
getStatusDescription,
isDefinedAndNotEmpty,
} from "helpers/others";
import { ContentWithTranslations } from "helpers/types"; import { ContentWithTranslations } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
@ -82,31 +86,29 @@ export default function Content(props: Props): JSX.Element {
horizontalLine horizontalLine
/> />
{selectedTranslation?.text_set && ( {selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && (
<div className="grid gap-5"> <div className="grid gap-5">
<h2 className="text-xl"> <h2 className="text-xl">
{selectedTranslation.text_set.source_language?.data?.attributes {selectedTranslation.text_set.source_language.data.attributes
?.code === selectedTranslation.language?.data?.attributes?.code .code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice ? langui.transcript_notice
: langui.translation_notice} : langui.translation_notice}
</h2> </h2>
{selectedTranslation.text_set.source_language?.data?.attributes {selectedTranslation.text_set.source_language.data.attributes.code !==
?.code && selectedTranslation.language?.data?.attributes?.code && (
selectedTranslation.text_set.source_language.data.attributes <div className="grid place-items-center gap-2">
.code !== <p className="font-headers">{langui.source_language}:</p>
selectedTranslation.language?.data?.attributes?.code && ( <Chip>
<div className="grid place-items-center gap-2"> {prettyLanguage(
<p className="font-headers">{langui.source_language}:</p> selectedTranslation.text_set.source_language.data.attributes
<Chip> .code,
{prettyLanguage( languages
selectedTranslation.text_set.source_language.data.attributes )}
.code, </Chip>
languages </div>
)} )}
</Chip>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2"> <div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers">{langui.status}:</p> <p className="font-headers">{langui.status}:</p>
@ -127,18 +129,16 @@ export default function Content(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{langui.transcribers}:</p> <p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedTranslation.text_set.transcribers.data.map( {filterHasAttributes(
(recorder) => ( selectedTranslation.text_set.transcribers.data
<Fragment key={recorder.id}> ).map((recorder) => (
{recorder.attributes && ( <Fragment key={recorder.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={recorder.attributes} recorder={recorder.attributes}
/> />
)} </Fragment>
</Fragment> ))}
)
)}
</div> </div>
</div> </div>
)} )}
@ -148,18 +148,16 @@ export default function Content(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{langui.translators}:</p> <p className="font-headers">{langui.translators}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedTranslation.text_set.translators.data.map( {filterHasAttributes(
(recorder) => ( selectedTranslation.text_set.translators.data
<Fragment key={recorder.id}> ).map((recorder) => (
{recorder.attributes && ( <Fragment key={recorder.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={recorder.attributes} recorder={recorder.attributes}
/> />
)} </Fragment>
</Fragment> ))}
)
)}
</div> </div>
</div> </div>
)} )}
@ -169,23 +167,21 @@ export default function Content(props: Props): JSX.Element {
<div> <div>
<p className="font-headers">{langui.proofreaders}:</p> <p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{selectedTranslation.text_set.proofreaders.data.map( {filterHasAttributes(
(recorder) => ( selectedTranslation.text_set.proofreaders.data
<Fragment key={recorder.id}> ).map((recorder) => (
{recorder.attributes && ( <Fragment key={recorder.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={recorder.attributes} recorder={recorder.attributes}
/> />
)} </Fragment>
</Fragment> ))}
)
)}
</div> </div>
</div> </div>
)} )}
{selectedTranslation.text_set.notes && ( {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div> <div>
<p className="font-headers">{"Notes"}:</p> <p className="font-headers">{"Notes"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
@ -450,13 +446,12 @@ export async function getStaticPaths(
const sdk = getReadySdk(); const sdk = getReadySdk();
const contents = await sdk.getContentsSlugs(); const contents = await sdk.getContentsSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
contents.contents?.data.map((item) => { filterHasAttributes(contents.contents?.data).map((item) => {
context.locales?.map((local) => { context.locales?.map((local) => {
if (item.attributes) paths.push({
paths.push({ params: { slug: item.attributes.slug },
params: { slug: item.attributes.slug }, locale: local,
locale: local, });
});
}); });
}); });
return { return {

View File

@ -21,6 +21,12 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import {
filterHasAttributes,
iterateMap,
mapRemoveEmptyValues,
} from "helpers/others";
import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
contents: NonNullable<GetContentsQuery["contents"]>["data"]; contents: NonNullable<GetContentsQuery["contents"]>["data"];
@ -128,8 +134,18 @@ export default function Contents(props: Props): JSX.Element {
); );
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.Full}> <ContentPanel width={ContentPanelWidthSizes.Full}>
{[...groups].map( {/* TODO: Add to langui */}
([name, items], index) => {groups.size === 0 && (
<ContentPlaceholder
message={
"No results. You can try changing or resetting the search parameters."
}
icon={Icon.ChevronLeft}
/>
)}
{iterateMap(
groups,
(name, items, index) =>
items.length > 0 && ( items.length > 0 && (
<Fragment key={index}> <Fragment key={index}>
{name && ( {name && (
@ -140,7 +156,10 @@ export default function Contents(props: Props): JSX.Element {
{name} {name}
<Chip>{`${items.reduce((currentSum, item) => { <Chip>{`${items.reduce((currentSum, item) => {
if (effectiveCombineRelatedContent) { if (effectiveCombineRelatedContent) {
if (item.attributes?.group?.data?.attributes?.combine) { if (
item.attributes?.group?.data?.attributes?.combine ===
true
) {
return ( return (
currentSum + currentSum +
(item.attributes.group.data.attributes.contents?.data (item.attributes.group.data.attributes.contents?.data
@ -161,50 +180,49 @@ export default function Contents(props: Props): JSX.Element {
className="grid grid-cols-2 items-end gap-8 className="grid grid-cols-2 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4" desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
> >
{items.map((item) => ( {filterHasAttributes(items).map((item) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{item.attributes && ( <TranslatedPreviewCard
<TranslatedPreviewCard href={`/contents/${item.attributes.slug}`}
href={`/contents/${item.attributes.slug}`} translations={item.attributes.translations?.map(
translations={item.attributes.translations?.map( (translation) => ({
(translation) => ({ pre_title: translation?.pre_title,
pre_title: translation?.pre_title, title: translation?.title,
title: translation?.title, subtitle: translation?.subtitle,
subtitle: translation?.subtitle, language:
language: translation?.language?.data?.attributes?.code,
translation?.language?.data?.attributes?.code, })
}) )}
)} slug={item.attributes.slug}
slug={item.attributes.slug} languages={languages}
languages={languages} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2"
thumbnailAspectRatio="3/2" thumbnailForceAspectRatio
thumbnailForceAspectRatio stackNumber={
stackNumber={ effectiveCombineRelatedContent &&
effectiveCombineRelatedContent && item.attributes.group?.data?.attributes?.combine ===
item.attributes.group?.data?.attributes?.combine true
? item.attributes.group.data.attributes.contents ? item.attributes.group.data.attributes.contents?.data
?.data.length .length
: 0 : 0
} }
topChips={ topChips={
item.attributes.type?.data?.attributes item.attributes.type?.data?.attributes
? [ ? [
item.attributes.type.data.attributes.titles?.[0] item.attributes.type.data.attributes.titles?.[0]
? item.attributes.type.data.attributes ? item.attributes.type.data.attributes.titles[0]
.titles[0]?.title ?.title
: prettySlug( : prettySlug(
item.attributes.type.data.attributes.slug item.attributes.type.data.attributes.slug
), ),
] ]
: undefined : undefined
} }
bottomChips={item.attributes.categories?.data.map( bottomChips={item.attributes.categories?.data.map(
(category) => category.attributes?.short ?? "" (category) => category.attributes?.short ?? ""
)} )}
keepInfoVisible={keepInfoVisible} keepInfoVisible={keepInfoVisible}
/> />
)}
</Fragment> </Fragment>
))} ))}
</div> </div>
@ -252,71 +270,72 @@ function getGroups(
groupByType: number, groupByType: number,
items: Props["contents"] items: Props["contents"]
): GroupContentItems { ): GroupContentItems {
const groups: GroupContentItems = new Map();
switch (groupByType) { switch (groupByType) {
case 0: { case 0: {
const group = new Map(); const noCategory = langui.no_category ?? "No category";
group.set("Drakengard 1", []); groups.set("Drakengard 1", []);
group.set("Drakengard 1.3", []); groups.set("Drakengard 1.3", []);
group.set("Drakengard 2", []); groups.set("Drakengard 2", []);
group.set("Drakengard 3", []); groups.set("Drakengard 3", []);
group.set("Drakengard 4", []); groups.set("Drakengard 4", []);
group.set("NieR Gestalt", []); groups.set("NieR Gestalt", []);
group.set("NieR Replicant", []); groups.set("NieR Replicant", []);
group.set("NieR Replicant ver.1.22474487139...", []); groups.set("NieR Replicant ver.1.22474487139...", []);
group.set("NieR:Automata", []); groups.set("NieR:Automata", []);
group.set("NieR Re[in]carnation", []); groups.set("NieR Re[in]carnation", []);
group.set("SINoALICE", []); groups.set("SINoALICE", []);
group.set("Voice of Cards", []); groups.set("Voice of Cards", []);
group.set("Final Fantasy XIV", []); groups.set("Final Fantasy XIV", []);
group.set("Thou Shalt Not Die", []); groups.set("Thou Shalt Not Die", []);
group.set("Bakuken", []); groups.set("Bakuken", []);
group.set("YoRHa", []); groups.set("YoRHa", []);
group.set("YoRHa Boys", []); groups.set("YoRHa Boys", []);
group.set(langui.no_category, []); groups.set(noCategory, []);
items.map((item) => { items.map((item) => {
if (item.attributes?.categories?.data.length === 0) { if (item.attributes?.categories?.data.length === 0) {
group.get(langui.no_category)?.push(item); groups.get(noCategory)?.push(item);
} else { } else {
item.attributes?.categories?.data.map((category) => { item.attributes?.categories?.data.map((category) => {
group.get(category.attributes?.name)?.push(item); groups.get(category.attributes?.name ?? noCategory)?.push(item);
}); });
} }
}); });
return group; break;
} }
case 1: { case 1: {
const group = new Map();
items.map((item) => { items.map((item) => {
const noType = langui.no_type ?? "No type";
const type = const type =
item.attributes?.type?.data?.attributes?.titles?.[0]?.title ?? item.attributes?.type?.data?.attributes?.titles?.[0]?.title ??
item.attributes?.type?.data?.attributes?.slug item.attributes?.type?.data?.attributes?.slug
? prettySlug(item.attributes.type.data.attributes.slug) ? prettySlug(item.attributes.type.data.attributes.slug)
: langui.no_type; : langui.no_type;
if (!group.has(type)) group.set(type, []); if (!groups.has(type ?? noType)) groups.set(type ?? noType, []);
group.get(type)?.push(item); groups.get(type ?? noType)?.push(item);
}); });
return group; break;
} }
default: { default: {
const group: GroupContentItems = new Map(); groups.set("", items);
group.set("", items);
return group;
} }
} }
return mapRemoveEmptyValues(groups);
} }
function filterContents( function filterContents(
contents: Immutable<Props["contents"]>, contents: Props["contents"],
combineRelatedContent: boolean, combineRelatedContent: boolean,
searchName: string searchName: string
): Immutable<Props["contents"]> { ): Props["contents"] {
return contents.filter((content) => { return contents.filter((content) => {
if ( if (
combineRelatedContent && combineRelatedContent &&
content.attributes?.group?.data?.attributes?.combine && content.attributes?.group?.data?.attributes?.combine === true &&
content.attributes.group.data.attributes.contents?.data[0].id !== content.attributes.group.data.attributes.contents?.data[0].id !==
content.id content.id
) { ) {

View File

@ -9,6 +9,7 @@ import { ToolTip } from "components/ToolTip";
import { DevGetContentsQuery } from "graphql/generated"; import { DevGetContentsQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
@ -116,321 +117,319 @@ function testingContent(contents: Props["contents"]): Report {
lines: [], lines: [],
}; };
contents.contents?.data.map((content) => { filterHasAttributes(contents.contents?.data).map((content) => {
if (content.attributes) { const backendUrl = `${process.env.NEXT_PUBLIC_URL_CMS}/admin/content-manager/collectionType/api::content.content/${content.id}`;
const backendUrl = `${process.env.NEXT_PUBLIC_URL_CMS}/admin/content-manager/collectionType/api::content.content/${content.id}`; const frontendUrl = `${process.env.NEXT_PUBLIC_URL_SELF}/contents/${content.attributes.slug}`;
const frontendUrl = `${process.env.NEXT_PUBLIC_URL_SELF}/contents/${content.attributes.slug}`;
if (content.attributes.categories?.data.length === 0) { if (content.attributes.categories?.data.length === 0) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Category", name: "No Category",
type: "Missing", type: "Missing",
severity: "Medium", severity: "Medium",
description: "The Content has no Category.", description: "The Content has no Category.",
recommandation: "Select a Category in relation with the Content", recommandation: "Select a Category in relation with the Content",
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (!content.attributes.type?.data?.id) { if (!content.attributes.type?.data?.id) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Category", name: "No Category",
type: "Missing", type: "Missing",
severity: "Medium", severity: "Medium",
description: "The Content has no Type.", description: "The Content has no Type.",
recommandation: 'If unsure, use the "Other" Type.', recommandation: 'If unsure, use the "Other" Type.',
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (content.attributes.ranged_contents?.data.length === 0) { if (content.attributes.ranged_contents?.data.length === 0) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Ranged Content", name: "No Ranged Content",
type: "Improvement", type: "Improvement",
severity: "Low", severity: "Low",
description: "The Content has no Ranged Content.", description: "The Content has no Ranged Content.",
recommandation: 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).", "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, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (!content.attributes.thumbnail?.data?.id) { if (!content.attributes.thumbnail?.data?.id) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Thumbnail", name: "No Thumbnail",
type: "Missing", type: "Missing",
severity: "High", severity: "High",
description: "The Content has no Thumbnail.", description: "The Content has no Thumbnail.",
recommandation: "", recommandation: "",
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
if (content.attributes.translations?.length === 0) { if (content.attributes.translations?.length === 0) {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [content.attributes.slug],
name: "No Titles", name: "No Titles",
type: "Missing", type: "Missing",
severity: "High", severity: "High",
description: "The Content has no Titles.", description: "The Content has no Titles.",
recommandation: "", recommandation: "",
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} else { } else {
const titleLanguages: string[] = []; const titleLanguages: string[] = [];
content.attributes.translations?.map((translation, titleIndex) => { filterDefined(content.attributes.translations).map(
if (translation && content.attributes) { (translation, titleIndex) => {
if (translation.language?.data?.id) { if (translation.language?.data?.id) {
if (translation.language.data.id in titleLanguages) { if (translation.language.data.id in titleLanguages) {
report.lines.push({
subitems: [
content.attributes.slug,
`Title ${titleIndex.toString()}`,
],
name: "Duplicate Language",
type: "Error",
severity: "High",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
titleLanguages.push(translation.language.data.id);
}
} else {
report.lines.push({ report.lines.push({
subitems: [ subitems: [
content.attributes.slug, content.attributes.slug,
`Title ${titleIndex.toString()}`, `Title ${titleIndex.toString()}`,
], ],
name: "No Language", name: "Duplicate Language",
type: "Error", type: "Error",
severity: "Very High", severity: "High",
description: "", description: "",
recommandation: "", recommandation: "",
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
}
if (!translation.description) {
report.lines.push({
subitems: [
content.attributes.slug,
`Title ${titleIndex.toString()}`,
],
name: "No Description",
type: "Missing",
severity: "Medium",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (translation.text_set) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Text Set",
type: "Missing",
severity: "Medium",
description: "The Content has no Text Set.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else { } else {
/* titleLanguages.push(translation.language.data.id);
*const textSetLanguages: string[] = [];
*if (content.attributes && textSet) {
* if (textSet.language?.data?.id) {
* if (textSet.language.data.id in textSetLanguages) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Duplicate Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* } else {
* textSetLanguages.push(textSet.language.data.id);
* }
* } else {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Language",
* type: "Error",
* severity: "Very High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.source_language?.data?.id) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Source Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Not Done Status",
* type: "Improvement",
* severity: "Low",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.text || textSet.text.length < 10) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Text",
* type: "Missing",
* severity: "Medium",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (
* textSet.source_language?.data?.id ===
* textSet.language?.data?.id
* ) {
* if (textSet.transcribers?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Transcribers",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Transcribers.",
* recommandation: "Add the appropriate Transcribers.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.translators?.data &&
* textSet.translators.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Translators",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Transcription but credits one or more Translators.",
* recommandation:
* "If appropriate, create a Translation Text Set with the Translator credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* } else {
* if (textSet.translators?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Translators",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Translators.",
* recommandation: "Add the appropriate Translators.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.transcribers?.data &&
* textSet.transcribers.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Transcribers",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Translation but credits one or more Transcribers.",
* recommandation:
* "If appropriate, create a Transcription Text Set with the Transcribers credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* }
*}
*/
} }
} else {
report.lines.push({ report.lines.push({
subitems: [content.attributes.slug], subitems: [
name: "No Sets", content.attributes.slug,
type: "Missing", `Title ${titleIndex.toString()}`,
severity: "Medium", ],
description: "The Content has no Sets.", name: "No Language",
type: "Error",
severity: "Very High",
description: "",
recommandation: "", recommandation: "",
backendUrl: backendUrl, backendUrl: backendUrl,
frontendUrl: frontendUrl, frontendUrl: frontendUrl,
}); });
} }
}); if (!translation.description) {
} report.lines.push({
subitems: [
content.attributes.slug,
`Title ${titleIndex.toString()}`,
],
name: "No Description",
type: "Missing",
severity: "Medium",
description: "",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
if (translation.text_set) {
report.lines.push({
subitems: [content.attributes.slug],
name: "No Text Set",
type: "Missing",
severity: "Medium",
description: "The Content has no Text Set.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
} else {
/*
*const textSetLanguages: string[] = [];
*if (content.attributes && textSet) {
* if (textSet.language?.data?.id) {
* if (textSet.language.data.id in textSetLanguages) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Duplicate Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* } else {
* textSetLanguages.push(textSet.language.data.id);
* }
* } else {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Language",
* type: "Error",
* severity: "Very High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.source_language?.data?.id) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Source Language",
* type: "Error",
* severity: "High",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (textSet.status !== Enum_Componentsetstextset_Status.Done) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Not Done Status",
* type: "Improvement",
* severity: "Low",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (!textSet.text || textSet.text.length < 10) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Text",
* type: "Missing",
* severity: "Medium",
* description: "",
* recommandation: "",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
*
* if (
* textSet.source_language?.data?.id ===
* textSet.language?.data?.id
* ) {
* if (textSet.transcribers?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Transcribers",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Transcribers.",
* recommandation: "Add the appropriate Transcribers.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.translators?.data &&
* textSet.translators.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Translators",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Transcription but credits one or more Translators.",
* recommandation:
* "If appropriate, create a Translation Text Set with the Translator credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* } else {
* if (textSet.translators?.data.length === 0) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "No Translators",
* type: "Missing",
* severity: "High",
* description:
* "The Content is a Transcription but doesn't credit any Translators.",
* recommandation: "Add the appropriate Translators.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* if (
* textSet.transcribers?.data &&
* textSet.transcribers.data.length > 0
* ) {
* report.lines.push({
* subitems: [
* content.attributes.slug,
* `TextSet ${textSetIndex.toString()}`,
* ],
* name: "Credited Transcribers",
* type: "Error",
* severity: "High",
* description:
* "The Content is a Translation but credits one or more Transcribers.",
* recommandation:
* "If appropriate, create a Transcription Text Set with the Transcribers credited there.",
* backendUrl: backendUrl,
* frontendUrl: frontendUrl,
* });
* }
* }
*}
*/
}
report.lines.push({
subitems: [content.attributes.slug],
name: "No Sets",
type: "Missing",
severity: "Medium",
description: "The Content has no Sets.",
recommandation: "",
backendUrl: backendUrl,
frontendUrl: frontendUrl,
});
}
);
} }
}); });
return report; return report;

View File

@ -35,7 +35,12 @@ import {
} from "helpers/formatters"; } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers"; import { convertMmToInch } from "helpers/numbers";
import { isDefined, sortContent } from "helpers/others"; import {
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
sortContent,
} from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
@ -172,7 +177,9 @@ export default function LibrarySlug(props: Props): JSX.Element {
)} )}
<div className="grid place-items-center text-center"> <div className="grid place-items-center text-center">
<h1 className="text-3xl">{item?.title}</h1> <h1 className="text-3xl">{item?.title}</h1>
{item?.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>} {item && isDefinedAndNotEmpty(item.subtitle) && (
<h2 className="text-2xl">{item.subtitle}</h2>
)}
</div> </div>
<PreviewCardCTAs <PreviewCardCTAs
@ -196,19 +203,15 @@ export default function LibrarySlug(props: Props): JSX.Element {
{item?.urls && item.urls.length ? ( {item?.urls && item.urls.length ? (
<div className="flex flex-row place-items-center gap-3"> <div className="flex flex-row place-items-center gap-3">
<p>{langui.available_at}</p> <p>{langui.available_at}</p>
{item.urls {filterHasAttributes(item.urls).map((url, index) => (
.filter((url) => url) <Fragment key={index}>
.map((url, index) => ( <Button
<Fragment key={index}> href={url.url}
{url?.url && ( target={"_blank"}
<Button text={prettyURL(url.url)}
href={url.url} />
target={"_blank"} </Fragment>
text={prettyURL(url.url)} ))}
/>
)}
</Fragment>
))}
</div> </div>
) : ( ) : (
<p>{langui.item_not_available}</p> <p>{langui.item_not_available}</p>
@ -225,26 +228,19 @@ export default function LibrarySlug(props: Props): JSX.Element {
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
gap-8" gap-8"
> >
{item.gallery.data.map((galleryItem, index) => ( {filterHasAttributes(item.gallery.data).map(
<Fragment key={galleryItem.id}> (galleryItem, index) => (
{galleryItem.attributes && ( <Fragment key={galleryItem.id}>
<div <div
className="relative aspect-square cursor-pointer className="relative aspect-square cursor-pointer
transition-transform hover:scale-[1.02]" transition-transform hover:scale-[1.02]"
onClick={() => { onClick={() => {
if (item.gallery?.data) { const images: string[] = filterHasAttributes(
const images: string[] = []; item.gallery?.data
item.gallery.data.map((image) => { ).map((image) =>
if (image.attributes) getAssetURL(image.attributes.url, ImageQuality.Large)
images.push( );
getAssetURL( openLightBox(images, index);
image.attributes.url,
ImageQuality.Large
)
);
});
openLightBox(images, index);
}
}} }}
> >
<Img <Img
@ -253,9 +249,9 @@ export default function LibrarySlug(props: Props): JSX.Element {
image={galleryItem.attributes} image={galleryItem.attributes}
/> />
</div> </div>
)} </Fragment>
</Fragment> )
))} )}
</div> </div>
</div> </div>
)} )}
@ -437,46 +433,44 @@ export default function LibrarySlug(props: Props): JSX.Element {
className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] className="grid w-full grid-cols-[repeat(auto-fill,minmax(15rem,1fr))]
items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1" items-end gap-8 mobile:grid-cols-2 thin:grid-cols-1"
> >
{item.subitems.data.map((subitem) => ( {filterHasAttributes(item.subitems.data).map((subitem) => (
<Fragment key={subitem.id}> <Fragment key={subitem.id}>
{subitem.attributes && subitem.id && ( <PreviewCard
<PreviewCard href={`/library/${subitem.attributes.slug}`}
href={`/library/${subitem.attributes.slug}`} title={subitem.attributes.title}
title={subitem.attributes.title} subtitle={subitem.attributes.subtitle}
subtitle={subitem.attributes.subtitle} thumbnail={subitem.attributes.thumbnail?.data?.attributes}
thumbnail={subitem.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="21/29.7"
thumbnailAspectRatio="21/29.7" thumbnailRounded={false}
thumbnailRounded={false} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} topChips={
topChips={ subitem.attributes.metadata &&
subitem.attributes.metadata && subitem.attributes.metadata.length > 0 &&
subitem.attributes.metadata.length > 0 && subitem.attributes.metadata[0]
subitem.attributes.metadata[0] ? [prettyItemSubType(subitem.attributes.metadata[0])]
? [prettyItemSubType(subitem.attributes.metadata[0])] : []
: [] }
} bottomChips={subitem.attributes.categories?.data.map(
bottomChips={subitem.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} metadata={{
metadata={{ currencies: currencies,
currencies: currencies, release_date: subitem.attributes.release_date,
release_date: subitem.attributes.release_date, price: subitem.attributes.price,
price: subitem.attributes.price, position: "Bottom",
position: "Bottom", }}
}} infoAppend={
infoAppend={ <PreviewCardCTAs
<PreviewCardCTAs id={subitem.id}
id={subitem.id} langui={langui}
langui={langui} displayCTAs={
displayCTAs={ !isUntangibleGroupItem(
!isUntangibleGroupItem( subitem.attributes.metadata?.[0]
subitem.attributes.metadata?.[0] )
) }
} />
/> }
} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>
@ -548,13 +542,12 @@ export async function getStaticPaths(
const sdk = getReadySdk(); const sdk = getReadySdk();
const libraryItems = await sdk.getLibraryItemsSlugs(); const libraryItems = await sdk.getLibraryItemsSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
if (libraryItems.libraryItems) { filterHasAttributes(libraryItems.libraryItems?.data).map((item) => {
libraryItems.libraryItems.data.map((item) => { context.locales?.map((local) =>
context.locales?.map((local) => { paths.push({ params: { slug: item.attributes?.slug }, locale: local })
paths.push({ params: { slug: item.attributes?.slug }, locale: local }); );
}); });
});
}
return { return {
paths, paths,
fallback: "blocking", fallback: "blocking",

View File

@ -15,7 +15,7 @@ import { GetLibraryItemScansQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { isDefined, sortContent } from "helpers/others"; import { filterHasAttributes, isDefined, sortContent } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { import {
@ -146,14 +146,12 @@ export async function getStaticPaths(
const sdk = getReadySdk(); const sdk = getReadySdk();
const libraryItems = await sdk.getLibraryItemsSlugs({}); const libraryItems = await sdk.getLibraryItemsSlugs({});
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
if (libraryItems.libraryItems) { filterHasAttributes(libraryItems.libraryItems?.data).map((item) => {
libraryItems.libraryItems.data.map((item) => { context.locales?.map((local) =>
context.locales?.map((local) => { paths.push({ params: { slug: item.attributes.slug }, locale: local })
if (item.attributes) );
paths.push({ params: { slug: item.attributes.slug }, locale: local }); });
});
});
}
return { return {
paths, paths,
fallback: "blocking", fallback: "blocking",

View File

@ -31,7 +31,13 @@ import {
import { PreviewCard } from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { isDefinedAndNotEmpty, isUndefined } from "helpers/others"; import {
filterHasAttributes,
isDefinedAndNotEmpty,
isUndefined,
iterateMap,
} from "helpers/others";
import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
@ -234,73 +240,74 @@ export default function Library(props: Props): JSX.Element {
); );
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.Full}> <ContentPanel width={ContentPanelWidthSizes.Full}>
{[...groups].map(([name, items]) => ( {/* TODO: Add to langui */}
{groups.size === 0 && (
<ContentPlaceholder
message={
"No results. You can try changing or resetting the search parameters."
}
icon={Icon.ChevronLeft}
/>
)}
{iterateMap(groups, (name, items) => (
<Fragment key={name}> <Fragment key={name}>
{items.length > 0 && ( {isDefinedAndNotEmpty(name) && (
<> <h2
{name && ( className="flex flex-row place-items-center gap-2
<h2
className="flex flex-row place-items-center gap-2
pb-2 pt-10 text-2xl first-of-type:pt-0" pb-2 pt-10 text-2xl first-of-type:pt-0"
> >
{name} {name}
<Chip>{`${items.length} ${ <Chip>{`${items.length} ${
items.length <= 1 items.length <= 1
? langui.result?.toLowerCase() ?? "result" ? langui.result?.toLowerCase() ?? "result"
: langui.results?.toLowerCase() ?? "results" : langui.results?.toLowerCase() ?? "results"
}`}</Chip> }`}</Chip>
</h2> </h2>
)} )}
<div <div
className="grid items-end gap-8 border-b-[3px] border-dotted pb-12 className="grid items-end gap-8 border-b-[3px] border-dotted pb-12
last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]
mobile:grid-cols-2 mobile:gap-4" mobile:grid-cols-2 mobile:gap-4"
> >
{items.map((item) => ( {filterHasAttributes(items).map((item) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{isDefinedAndNotEmpty(item.id) && item.attributes && ( <PreviewCard
<PreviewCard href={`/library/${item.attributes.slug}`}
href={`/library/${item.attributes.slug}`} title={item.attributes.title}
title={item.attributes.title} subtitle={item.attributes.subtitle}
subtitle={item.attributes.subtitle} thumbnail={item.attributes.thumbnail?.data?.attributes}
thumbnail={item.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="21/29.7"
thumbnailAspectRatio="21/29.7" thumbnailRounded={false}
thumbnailRounded={false} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} topChips={
topChips={ item.attributes.metadata &&
item.attributes.metadata && item.attributes.metadata.length > 0 &&
item.attributes.metadata.length > 0 && item.attributes.metadata[0]
item.attributes.metadata[0] ? [prettyItemSubType(item.attributes.metadata[0])]
? [prettyItemSubType(item.attributes.metadata[0])] : []
: [] }
} bottomChips={item.attributes.categories?.data.map(
bottomChips={item.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} metadata={{
metadata={{ currencies: currencies,
currencies: currencies, release_date: item.attributes.release_date,
release_date: item.attributes.release_date, price: item.attributes.price,
price: item.attributes.price, position: "Bottom",
position: "Bottom", }}
}} infoAppend={
infoAppend={ <PreviewCardCTAs
<PreviewCardCTAs id={item.id}
id={item.id} displayCTAs={
displayCTAs={ !isUntangibleGroupItem(item.attributes.metadata?.[0])
!isUntangibleGroupItem( }
item.attributes.metadata?.[0] langui={langui}
) />
} }
langui={langui} />
/> </Fragment>
} ))}
/> </div>
)}
</Fragment>
))}
</div>
</>
)}
</Fragment> </Fragment>
))} ))}
</ContentPanel> </ContentPanel>

View File

@ -5,7 +5,7 @@ import {
PostStaticProps, PostStaticProps,
} from "graphql/getPostStaticProps"; } from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
@ -48,13 +48,12 @@ export async function getStaticPaths(
const sdk = getReadySdk(); const sdk = getReadySdk();
const posts = await sdk.getPostsSlugs(); const posts = await sdk.getPostsSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
if (posts.posts)
posts.posts.data.map((item) => { filterHasAttributes(posts.posts?.data).map((item) => {
context.locales?.map((local) => { context.locales?.map((local) =>
if (item.attributes) paths.push({ params: { slug: item.attributes.slug }, locale: local })
paths.push({ params: { slug: item.attributes.slug }, locale: local }); );
}); });
});
return { return {
paths, paths,
fallback: "blocking", fallback: "blocking",

View File

@ -19,6 +19,7 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { filterHasAttributes } from "helpers/others";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"]; posts: NonNullable<GetPostsPreviewQuery["posts"]>["data"];
@ -87,29 +88,27 @@ export default function News(props: Props): JSX.Element {
className="grid grid-cols-1 items-end gap-8 className="grid grid-cols-1 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]" desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))]"
> >
{filteredItems.map((post) => ( {filterHasAttributes(filteredItems).map((post) => (
<Fragment key={post.id}> <Fragment key={post.id}>
{post.attributes && ( <PreviewCard
<PreviewCard href={`/news/${post.attributes.slug}`}
href={`/news/${post.attributes.slug}`} title={
title={ post.attributes.translations?.[0]?.title ??
post.attributes.translations?.[0]?.title ?? prettySlug(post.attributes.slug)
prettySlug(post.attributes.slug) }
} description={post.attributes.translations?.[0]?.excerpt}
description={post.attributes.translations?.[0]?.excerpt} thumbnail={post.attributes.thumbnail?.data?.attributes}
thumbnail={post.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio="3/2"
thumbnailAspectRatio="3/2" thumbnailForceAspectRatio
thumbnailForceAspectRatio bottomChips={post.attributes.categories?.data.map(
bottomChips={post.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} metadata={{
metadata={{ release_date: post.attributes.date,
release_date: post.attributes.date, position: "Top",
position: "Top", }}
}} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>
@ -145,19 +144,17 @@ export async function getStaticProps(
} }
function sortPosts(posts: Props["posts"]): Props["posts"] { function sortPosts(posts: Props["posts"]): Props["posts"] {
const sortedPosts = [...posts]; return posts
sortedPosts
.sort((a, b) => { .sort((a, b) => {
const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999"; const dateA = a.attributes?.date ? prettyDate(a.attributes.date) : "9999";
const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999"; const dateB = b.attributes?.date ? prettyDate(b.attributes.date) : "9999";
return dateA.localeCompare(dateB); return dateA.localeCompare(dateB);
}) })
.reverse(); .reverse();
return sortedPosts;
} }
function filterItems(posts: Props["posts"], searchName: string) { function filterItems(posts: Props["posts"], searchName: string) {
return [...posts].filter((post) => { return posts.filter((post) => {
if (searchName.length > 1) { if (searchName.length > 1) {
if ( if (
post.attributes?.translations?.[0]?.title post.attributes?.translations?.[0]?.title

View File

@ -14,7 +14,11 @@ import { SubPanel } from "components/Panels/SubPanel";
import DefinitionCard from "components/Wiki/DefinitionCard"; import DefinitionCard from "components/Wiki/DefinitionCard";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import {
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/others";
import { WikiPageWithTranslations } from "helpers/types"; import { WikiPageWithTranslations } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { import {
@ -89,20 +93,24 @@ export default function WikiPage(props: Props): JSX.Element {
</div> </div>
)} )}
{page.definitions?.map((definition, index) => ( {filterHasAttributes(page.definitions, ["translations"]).map(
<DefinitionCard (definition, index) => (
key={index} <DefinitionCard
source={definition?.source?.data?.attributes?.name} key={index}
translations={definition?.translations?.map((translation) => ({ source={definition.source?.data?.attributes?.name}
language: translation?.language?.data?.attributes?.code, translations={filterHasAttributes(definition.translations).map(
definition: translation?.definition, (translation) => ({
status: translation?.status, language: translation.language.data?.attributes?.code,
}))} definition: translation.definition,
index={index + 1} status: translation.status,
languages={languages} })
langui={langui} )}
/> index={index + 1}
))} languages={languages}
langui={langui}
/>
)
)}
</div> </div>
)} )}
</ContentPanel> </ContentPanel>
@ -147,14 +155,13 @@ export async function getStaticPaths(
const sdk = getReadySdk(); const sdk = getReadySdk();
const contents = await sdk.getWikiPagesSlugs(); const contents = await sdk.getWikiPagesSlugs();
const paths: GetStaticPathsResult["paths"] = []; const paths: GetStaticPathsResult["paths"] = [];
contents.wikiPages?.data.map((wikiPage) => { filterHasAttributes(contents.wikiPages?.data).map((wikiPage) => {
context.locales?.map((local) => { context.locales?.map((local) =>
if (wikiPage.attributes) paths.push({
paths.push({ params: { slug: wikiPage.attributes.slug },
params: { slug: wikiPage.attributes.slug }, locale: local,
locale: local, })
}); );
});
}); });
return { return {
paths, paths,

View File

@ -12,7 +12,7 @@ import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment } from "react"; import { Fragment } from "react";
@ -70,22 +70,20 @@ export default function Chronology(props: Props): JSX.Element {
horizontalLine horizontalLine
/> />
{chronologyEras.map((era) => ( {filterHasAttributes(chronologyEras).map((era) => (
<Fragment key={era.id}> <Fragment key={era.id}>
{era.attributes && ( <NavOption
<NavOption url={`#${era.attributes.slug}`}
url={`#${era.attributes.slug}`} title={
title={ era.attributes.title &&
era.attributes.title && era.attributes.title.length > 0 &&
era.attributes.title.length > 0 && era.attributes.title[0]
era.attributes.title[0] ? era.attributes.title[0].title
? era.attributes.title[0].title : prettySlug(era.attributes.slug)
: prettySlug(era.attributes.slug) }
} subtitle={`${era.attributes.starting_year}${era.attributes.ending_year}`}
subtitle={`${era.attributes.starting_year}${era.attributes.ending_year}`} border
border />
/>
)}
</Fragment> </Fragment>
))} ))}
</SubPanel> </SubPanel>

View File

@ -20,6 +20,8 @@ import { Switch } from "components/Inputs/Switch";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { filterHasAttributes } from "helpers/others";
import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
pages: NonNullable<GetWikiPagesPreviewsQuery["wikiPages"]>["data"]; pages: NonNullable<GetWikiPagesPreviewsQuery["wikiPages"]>["data"];
@ -93,30 +95,37 @@ export default function Wiki(props: Props): JSX.Element {
className="grid grid-cols-2 items-end gap-8 className="grid grid-cols-2 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4" desktop:grid-cols-[repeat(auto-fill,_minmax(20rem,1fr))] mobile:gap-4"
> >
{filteredPages.map((page) => ( {/* TODO: Add to langui */}
{filteredPages.length === 0 && (
<ContentPlaceholder
message={
"No results. You can try changing or resetting the search parameters."
}
icon={Icon.ChevronLeft}
/>
)}
{filterHasAttributes(filteredPages).map((page) => (
<Fragment key={page.id}> <Fragment key={page.id}>
{page.attributes && ( <TranslatedPreviewCard
<TranslatedPreviewCard href={`/wiki/${page.attributes.slug}`}
href={`/wiki/${page.attributes.slug}`} translations={page.attributes.translations?.map(
translations={page.attributes.translations?.map( (translation) => ({
(translation) => ({ title: translation?.title,
title: translation?.title, description: translation?.summary,
description: translation?.summary, language: translation?.language?.data?.attributes?.code,
language: translation?.language?.data?.attributes?.code, })
}) )}
)} thumbnail={page.attributes.thumbnail?.data?.attributes}
thumbnail={page.attributes.thumbnail?.data?.attributes} thumbnailAspectRatio={"4/3"}
thumbnailAspectRatio={"4/3"} thumbnailRounded
thumbnailRounded thumbnailForceAspectRatio
thumbnailForceAspectRatio languages={languages}
languages={languages} slug={page.attributes.slug}
slug={page.attributes.slug} keepInfoVisible={keepInfoVisible}
keepInfoVisible={keepInfoVisible} bottomChips={page.attributes.categories?.data.map(
bottomChips={page.attributes.categories?.data.map( (category) => category.attributes?.short ?? ""
(category) => category.attributes?.short ?? "" )}
)} />
/>
)}
</Fragment> </Fragment>
))} ))}
</div> </div>
@ -150,17 +159,15 @@ export async function getStaticProps(
} }
function sortPages(pages: Props["pages"]): Props["pages"] { function sortPages(pages: Props["pages"]): Props["pages"] {
const sortedPages = [...pages]; return pages.sort((a, b) => {
sortedPages.sort((a, b) => {
const slugA = a.attributes?.slug ?? ""; const slugA = a.attributes?.slug ?? "";
const slugB = b.attributes?.slug ?? ""; const slugB = b.attributes?.slug ?? "";
return slugA.localeCompare(slugB); return slugA.localeCompare(slugB);
}); });
return sortedPages;
} }
function filterPages(posts: Props["pages"], searchName: string) { function filterPages(posts: Props["pages"], searchName: string) {
return [...posts].filter((post) => { return posts.filter((post) => {
if (searchName.length > 1) { if (searchName.length > 1) {
if ( if (
post.attributes?.translations?.[0]?.title post.attributes?.translations?.[0]?.title

View File

@ -31,7 +31,7 @@ mark {
/* SCROLLBARS STYLING */ /* SCROLLBARS STYLING */
* { * {
@apply [scrollbar-color:theme(colors.dark)_transparent] [scrollbar-width:thin]; @apply [scrollbar-color:theme(colors.dark/1)_transparent] [scrollbar-width:thin];
} }
*::-webkit-scrollbar { *::-webkit-scrollbar {