useBoolean + many fixes

This commit is contained in:
DrMint 2022-07-13 03:46:58 +02:00
parent b6c2363093
commit 260bdd5577
36 changed files with 1065 additions and 737 deletions

View File

@ -6,4 +6,5 @@ next-sitemap.config.js
next.config.js next.config.js
postcss.config.js postcss.config.js
tailwind.config.js tailwind.config.js
design.config.js design.config.js
graphql.config.js

View File

@ -30,7 +30,6 @@ The following is all the tests done on the data entries coming from Strapi. This
| Text Sets | Credited Translators | Error | High | The Content is a Transcription but credits one or more Translators. | If appropriate, create a Translation Text Set with the Translator credited there. | | Text Sets | Credited Translators | Error | High | The Content is a Transcription but credits one or more Translators. | If appropriate, create a Translation Text Set with the Translator credited there. |
| Text Sets | Duplicate Language | Error | High | | | | Text Sets | Duplicate Language | Error | High | | |
## LibraryItems ## LibraryItems
| Subitem | Name | Type | Severity | Description | Recommendation | | Subitem | Name | Type | Severity | Description | Recommendation |
@ -100,4 +99,4 @@ The following is all the tests done on the data entries coming from Strapi. This
| Metadata Group | Has URLs | Error | High | Variant Sets shouldn't have URLs. | | | Metadata Group | Has URLs | Error | High | Variant Sets shouldn't have URLs. | |
| Metadata Group | Has Contents | Error | High | Variant Sets and Relation Set shouldn't have Contents. | | | Metadata Group | Has Contents | Error | High | Variant Sets and Relation Set shouldn't have Contents. | |
| Metadata Group | Has Images | Error | High | Variant Sets and Relation Set shouldn't have Images. | | | Metadata Group | Has Images | Error | High | Variant Sets and Relation Set shouldn't have Images. | |
| Metadata Group | No Subitems | Missing | High | Group Items should have subitems. | | Metadata Group | No Subitems | Missing | High | Group Items should have subitems. |

19
graphql.config.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
projects: {
app: {
schema: process.env.URL_GRAPHQL,
documents: [
"src/graphql/operations/*.graphql",
"src/graphql/fragments/*.graphql",
],
extensions: {
endpoints: {
default: {
url: process.env.URL_GRAPHQL,
headers: { Authorization: `Bearer ${process.env.ACCESS_TOKEN}` },
},
},
},
},
},
};

View File

@ -97,14 +97,13 @@ export const AppLayout = ({
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition router.events.on("routeChangeStart", () => {
router.events?.on("routeChangeStart", () => {
setConfigPanelOpen(false); setConfigPanelOpen(false);
setMainPanelOpen(false); setMainPanelOpen(false);
setSubPanelOpen(false); setSubPanelOpen(false);
}); });
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
router.events?.on("hashChangeStart", () => { router.events.on("hashChangeStart", () => {
setSubPanelOpen(false); setSubPanelOpen(false);
}); });
}, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]); }, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]);
@ -461,8 +460,8 @@ export const AppLayout = ({
<div> <div>
<Select <Select
options={currencyOptions} options={currencyOptions}
state={currencySelect} value={currencySelect}
setState={setCurrencySelect} onChange={setCurrencySelect}
className="w-28" className="w-28"
/> />
</div> </div>
@ -516,8 +515,8 @@ export const AppLayout = ({
<TextInput <TextInput
placeholder="<player>" placeholder="<player>"
className="w-48" className="w-48"
state={playerName} value={playerName ?? ""}
setState={setPlayerName} onChange={setPlayerName}
/> />
</div> </div>
</div> </div>

View File

@ -64,13 +64,13 @@ export const Button = ({
id={id} id={id}
onClick={onClick} onClick={onClick}
className={cJoin( className={cJoin(
`group grid select-none grid-flow-col place-content-center `group grid cursor-pointer select-none grid-flow-col
place-items-center gap-2 rounded-full border-[1px] border-dark py-3 px-4 leading-none place-content-center place-items-center gap-2 rounded-full border-[1px] border-dark py-3 px-4
text-dark transition-all`, leading-none 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" "hover:bg-dark hover:text-light hover:drop-shadow-shade-lg"
), ),
cIf(size === "small", "px-3 py-1 text-xs"), cIf(size === "small", "px-3 py-1 text-xs"),
className className

View File

@ -1,4 +1,3 @@
import { Dispatch, SetStateAction } from "react";
import { ButtonGroup } from "./ButtonGroup"; import { ButtonGroup } from "./ButtonGroup";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
@ -10,34 +9,23 @@ import { cJoin } from "helpers/className";
interface Props { interface Props {
className?: string; className?: string;
maxPage: number;
page: number; page: number;
setPage: Dispatch<SetStateAction<number>>; onChange: (value: number) => void;
} }
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const PageSelector = ({ export const PageSelector = ({
page, page,
setPage,
maxPage,
className, className,
onChange,
}: Props): JSX.Element => ( }: Props): JSX.Element => (
<ButtonGroup <ButtonGroup
className={cJoin("flex flex-row place-content-center", className)} className={cJoin("flex flex-row place-content-center", className)}
buttonsProps={[ buttonsProps={[
{ { onClick: () => onChange(page - 1), icon: Icon.NavigateBefore },
onClick: () => setPage((current) => (page > 0 ? current - 1 : current)), { text: (page + 1).toString() },
icon: Icon.NavigateBefore, { onClick: () => onChange(page + 1), icon: Icon.NavigateNext },
},
{
text: (page + 1).toString(),
},
{
onClick: () =>
setPage((current) => (page < maxPage ? page + 1 : current)),
icon: Icon.NavigateNext,
},
]} ]}
/> />
); );

View File

@ -1,10 +1,4 @@
import { import { Fragment, useCallback, useState } from "react";
Dispatch,
Fragment,
SetStateAction,
useCallback,
useState,
} from "react";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { useToggle } from "hooks/useToggle"; import { useToggle } from "hooks/useToggle";
@ -15,30 +9,30 @@ import { useToggle } from "hooks/useToggle";
*/ */
interface Props { interface Props {
setState: Dispatch<SetStateAction<number>>; value: number;
state: number;
options: string[]; options: string[];
selected?: number; selected?: number;
allowEmpty?: boolean; allowEmpty?: boolean;
className?: string; className?: string;
onChange: (value: number) => void;
} }
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const Select = ({ export const Select = ({
className, className,
state, value,
options, options,
allowEmpty, allowEmpty,
setState, onChange,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const toggleOpened = useToggle(setOpened); const toggleOpened = useToggle(setOpened);
const tryToggling = useCallback(() => { const tryToggling = useCallback(() => {
const optionCount = options.length + (state === -1 ? 1 : 0); const optionCount = options.length + (value === -1 ? 1 : 0);
if (optionCount > 1) toggleOpened(); if (optionCount > 1) toggleOpened();
}, [options.length, state, toggleOpened]); }, [options.length, value, toggleOpened]);
return ( return (
<div <div
@ -57,14 +51,14 @@ export const Select = ({
)} )}
> >
<p onClick={tryToggling} className="w-full"> <p onClick={tryToggling} className="w-full">
{state === -1 ? "—" : options[state]} {value === -1 ? "—" : options[value]}
</p> </p>
{state >= 0 && allowEmpty && ( {value >= 0 && allowEmpty && (
<Ico <Ico
icon={Icon.Close} icon={Icon.Close}
className="!text-xs" className="!text-xs"
onClick={() => { onClick={() => {
setState(-1); onChange(-1);
setOpened(false); setOpened(false);
}} }}
/> />
@ -82,7 +76,7 @@ export const Select = ({
> >
{options.map((option, index) => ( {options.map((option, index) => (
<Fragment key={index}> <Fragment key={index}>
{index !== state && ( {index !== value && (
<div <div
className={cJoin( className={cJoin(
"cursor-pointer p-1 transition-colors last-of-type:rounded-b-[1em] hover:bg-mid", "cursor-pointer p-1 transition-colors last-of-type:rounded-b-[1em] hover:bg-mid",
@ -91,7 +85,7 @@ export const Select = ({
id={option} id={option}
onClick={() => { onClick={() => {
setOpened(false); setOpened(false);
setState(index); onChange(index);
}} }}
> >
{option} {option}

View File

@ -1,6 +1,4 @@
import { Dispatch, SetStateAction } from "react";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { useToggle } from "hooks/useToggle";
/* /*
* *
@ -8,8 +6,8 @@ import { useToggle } from "hooks/useToggle";
*/ */
interface Props { interface Props {
setState: Dispatch<SetStateAction<boolean>>; onClick: () => void;
state: boolean; value: boolean;
className?: string; className?: string;
disabled?: boolean; disabled?: boolean;
} }
@ -17,38 +15,31 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const Switch = ({ export const Switch = ({
state, value,
setState, onClick,
className, className,
disabled = false, disabled = false,
}: Props): JSX.Element => { }: Props): JSX.Element => (
const toggleState = useToggle(setState); <div
return ( className={cJoin(
"relative grid h-6 w-12 rounded-full border-2 border-mid transition-colors",
cIf(disabled, "cursor-not-allowed", "cursor-pointer"),
cIf(value, "border-none bg-mid shadow-inner-sm shadow-shade", "bg-light"),
className
)}
onClick={() => {
if (!disabled) onClick();
}}
>
<div <div
className={cJoin( className={cJoin(
"relative grid h-6 w-12 rounded-full border-2 border-mid transition-colors", "absolute aspect-square rounded-full bg-dark transition-transform",
cIf(disabled, "cursor-not-allowed", "cursor-pointer"),
cIf( cIf(
state, value,
"border-none bg-mid shadow-inner-sm shadow-shade", "top-[2px] bottom-[2px] left-[2px] translate-x-[120%]",
"bg-light" "top-0 bottom-0 left-0"
), )
className
)} )}
onClick={() => { ></div>
if (!disabled) toggleState(); </div>
}} );
>
<div
className={cJoin(
"absolute aspect-square rounded-full bg-dark transition-transform",
cIf(
state,
"top-[2px] bottom-[2px] left-[2px] translate-x-[120%]",
"top-0 bottom-0 left-0"
)
)}
></div>
</div>
);
};

View File

@ -1,4 +1,3 @@
import { Dispatch, SetStateAction } from "react";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others"; import { isDefinedAndNotEmpty } from "helpers/others";
@ -9,10 +8,8 @@ import { isDefinedAndNotEmpty } from "helpers/others";
*/ */
interface Props { interface Props {
state: string | undefined; value: string;
setState: onChange: (newValue: string) => void;
| Dispatch<SetStateAction<string | undefined>>
| Dispatch<SetStateAction<string>>;
className?: string; className?: string;
name?: string; name?: string;
placeholder?: string; placeholder?: string;
@ -21,8 +18,8 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TextInput = ({ export const TextInput = ({
state, value,
setState, onChange,
className, className,
name, name,
placeholder, placeholder,
@ -32,19 +29,19 @@ export const TextInput = ({
className="w-full" className="w-full"
type="text" type="text"
name={name} name={name}
value={state} value={value}
placeholder={placeholder} placeholder={placeholder}
onChange={(event) => { onChange={(event) => {
setState(event.target.value); onChange(event.target.value);
}} }}
/> />
{isDefinedAndNotEmpty(state) && ( {isDefinedAndNotEmpty(value) && (
<div className="absolute right-4 top-0 bottom-0 grid place-items-center"> <div className="absolute right-4 top-0 bottom-0 grid place-items-center">
<Ico <Ico
className="cursor-pointer !text-xs" className="cursor-pointer !text-xs"
icon={Icon.Close} icon={Icon.Close}
onClick={() => { onClick={() => {
setState(""); onChange("");
}} }}
/> />
</div> </div>

View File

@ -23,7 +23,7 @@ export const PreviewCardCTAs = ({
expand = false, expand = false,
langui, langui,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const appLayout = useAppLayout(); const { libraryItemUserStatus, setLibraryItemUserStatus } = useAppLayout();
return ( return (
<> <>
<div <div
@ -35,13 +35,10 @@ export const PreviewCardCTAs = ({
<Button <Button
icon={Icon.Favorite} icon={Icon.Favorite}
text={expand ? langui.want_it : undefined} text={expand ? langui.want_it : undefined}
active={ active={libraryItemUserStatus?.[id] === LibraryItemUserStatus.Want}
appLayout.libraryItemUserStatus?.[id] ===
LibraryItemUserStatus.Want
}
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
appLayout.setLibraryItemUserStatus((current) => { setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = current ? { ...current } : {}; const newLibraryItemUserStatus = current ? { ...current } : {};
newLibraryItemUserStatus[id] = newLibraryItemUserStatus[id] =
newLibraryItemUserStatus[id] === LibraryItemUserStatus.Want newLibraryItemUserStatus[id] === LibraryItemUserStatus.Want
@ -56,13 +53,10 @@ export const PreviewCardCTAs = ({
<Button <Button
icon={Icon.BackHand} icon={Icon.BackHand}
text={expand ? langui.have_it : undefined} text={expand ? langui.have_it : undefined}
active={ active={libraryItemUserStatus?.[id] === LibraryItemUserStatus.Have}
appLayout.libraryItemUserStatus?.[id] ===
LibraryItemUserStatus.Have
}
onClick={(event) => { onClick={(event) => {
event.preventDefault(); event.preventDefault();
appLayout.setLibraryItemUserStatus((current) => { setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = current ? { ...current } : {}; const newLibraryItemUserStatus = current ? { ...current } : {};
newLibraryItemUserStatus[id] = newLibraryItemUserStatus[id] =
newLibraryItemUserStatus[id] === LibraryItemUserStatus.Have newLibraryItemUserStatus[id] === LibraryItemUserStatus.Have

View File

@ -34,7 +34,7 @@ interface Props {
>["data"][number]["attributes"] >["data"][number]["attributes"]
>["scan_set"] >["scan_set"]
>; >;
slug: string; id: string;
title: string; title: string;
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
@ -51,10 +51,10 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const ScanSet = ({ const ScanSet = ({
openLightBox, openLightBox,
scanSet, scanSet,
slug, id,
title, title,
languages, languages,
langui, langui,
@ -115,17 +115,16 @@ export const ScanSet = ({
className="flex flex-row flex-wrap place-items-center className="flex flex-row flex-wrap place-items-center
gap-6 pt-10 text-base first-of-type:pt-0" gap-6 pt-10 text-base first-of-type:pt-0"
> >
<h2 id={slug} className="text-2xl"> <h2 id={id} className="text-2xl">
{title} {title}
</h2> </h2>
{/* TODO: Add Scan and Scanlation to langui */}
<Chip <Chip
text={ text={
selectedScan.language?.data?.attributes?.code === selectedScan.language?.data?.attributes?.code ===
selectedScan.source_language?.data?.attributes?.code selectedScan.source_language?.data?.attributes?.code
? "Scan" ? langui.scan ?? "Scan"
: "Scanlation" : langui.scanlation ?? "Scanlation"
} }
/> />
</div> </div>
@ -153,8 +152,7 @@ export const ScanSet = ({
{selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( {selectedScan.scanners && selectedScan.scanners.data.length > 0 && (
<div> <div>
{/* TODO: Add Scanner to langui */} <p className="font-headers font-bold">{langui.scanners}:</p>
<p className="font-headers font-bold">{"Scanners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.scanners.data, [ {filterHasAttributes(selectedScan.scanners.data, [
"id", "id",
@ -173,8 +171,7 @@ export const ScanSet = ({
{selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && (
<div> <div>
{/* TODO: Add Cleaners to langui */} <p className="font-headers font-bold">{langui.cleaners}:</p>
<p className="font-headers font-bold">{"Cleaners"}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.cleaners.data, [ {filterHasAttributes(selectedScan.cleaners.data, [
"id", "id",
@ -194,8 +191,9 @@ export const ScanSet = ({
{selectedScan.typesetters && {selectedScan.typesetters &&
selectedScan.typesetters.data.length > 0 && ( selectedScan.typesetters.data.length > 0 && (
<div> <div>
{/* TODO: Add typesetter to langui */} <p className="font-headers font-bold">
<p className="font-headers font-bold">{"Typesetters"}:</p> {langui.typesetters}:
</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.typesetters.data, [ {filterHasAttributes(selectedScan.typesetters.data, [
"id", "id",
@ -214,8 +212,7 @@ export const ScanSet = ({
{isDefinedAndNotEmpty(selectedScan.notes) && ( {isDefinedAndNotEmpty(selectedScan.notes) && (
<ToolTip content={selectedScan.notes}> <ToolTip content={selectedScan.notes}>
{/* TODO: Add Notes to langui */} <Chip text={langui.notes ?? "Notes"} />
<Chip text={"Notes"} />
</ToolTip> </ToolTip>
)} )}
</div> </div>
@ -245,3 +242,40 @@ export const ScanSet = ({
</> </>
); );
}; };
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface TranslatedProps extends Omit<Props, "title"> {
translations: {
title: string;
language: string;
}[];
fallbackTitle: TranslatedProps["translations"][number]["title"];
languages: AppStaticProps["languages"];
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TranslatedScanSet = ({
fallbackTitle,
translations = [{ title: fallbackTitle, language: "default" }],
languages,
...otherProps
}: TranslatedProps): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languages: languages,
languageExtractor: useCallback(
(item: TranslatedProps["translations"][number]) => item.language,
[]
),
});
return (
<ScanSet
title={selectedTranslation?.title ?? fallbackTitle}
languages={languages}
{...otherProps}
/>
);
};

View File

@ -66,132 +66,126 @@ export const ScanSetCover = ({
return memo; return memo;
}, [selectedScan]); }, [selectedScan]);
if (coverImages.length > 0) { return (
return ( <>
<> {coverImages.length > 0 && selectedScan && (
{selectedScan && ( <div>
<div> <div
<div className="flex flex-row flex-wrap place-items-center
className="flex flex-row flex-wrap place-items-center
gap-6 pt-10 text-base first-of-type:pt-0" gap-6 pt-10 text-base first-of-type:pt-0"
> >
<h2 id={"cover"} className="text-2xl"> <h2 id={"cover"} className="text-2xl">
{/* TODO: Add Cover to langui */} {langui.cover}
{"Cover"} </h2>
</h2>
{/* TODO: Add Scan and Scanlation to langui */} <Chip
<Chip text={
text={ selectedScan.language?.data?.attributes?.code ===
selectedScan.language?.data?.attributes?.code === selectedScan.source_language?.data?.attributes?.code
selectedScan.source_language?.data?.attributes?.code ? langui.scan ?? "Scan"
? "Scan" : langui.scanlation ?? "Scanlation"
: "Scanlation" }
} />
/> </div>
<div className="flex flex-row flex-wrap place-items-center gap-4 pb-6">
<LanguageSwitcher {...languageSwitcherProps} />
<div className="grid place-content-center place-items-center">
<p className="font-headers font-bold">{langui.status}:</p>
<ToolTip
content={getStatusDescription(selectedScan.status, langui)}
maxWidth={"20rem"}
>
<Chip text={selectedScan.status} />
</ToolTip>
</div> </div>
<div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> {selectedScan.scanners && selectedScan.scanners.data.length > 0 && (
<LanguageSwitcher {...languageSwitcherProps} /> <div>
<p className="font-headers font-bold">{langui.scanners}:</p>
<div className="grid place-content-center place-items-center"> <div className="grid place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p> {filterHasAttributes(selectedScan.scanners.data, [
<ToolTip "id",
content={getStatusDescription(selectedScan.status, langui)} "attributes",
maxWidth={"20rem"} ] as const).map((scanner) => (
> <Fragment key={scanner.id}>
<Chip text={selectedScan.status} /> <RecorderChip
</ToolTip> langui={langui}
recorder={scanner.attributes}
/>
</Fragment>
))}
</div>
</div> </div>
)}
{selectedScan.scanners && selectedScan.scanners.data.length > 0 && ( {selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && (
<div>
<p className="font-headers font-bold">{langui.cleaners}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.cleaners.data, [
"id",
"attributes",
] as const).map((cleaner) => (
<Fragment key={cleaner.id}>
<RecorderChip
langui={langui}
recorder={cleaner.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedScan.typesetters &&
selectedScan.typesetters.data.length > 0 && (
<div> <div>
{/* TODO: Add Scanner to langui */} <p className="font-headers font-bold">
<p className="font-headers font-bold">{"Scanners"}:</p> {langui.typesetters}:
</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.scanners.data, [ {filterHasAttributes(selectedScan.typesetters.data, [
"id", "id",
"attributes", "attributes",
] as const).map((scanner) => ( ] as const).map((typesetter) => (
<Fragment key={scanner.id}> <Fragment key={typesetter.id}>
<RecorderChip <RecorderChip
langui={langui} langui={langui}
recorder={scanner.attributes} recorder={typesetter.attributes}
/> />
</Fragment> </Fragment>
))} ))}
</div> </div>
</div> </div>
)} )}
</div>
{selectedScan.cleaners && selectedScan.cleaners.data.length > 0 && ( <div
<div> className="grid items-end gap-8 border-b-[3px] border-dotted pb-12
{/* TODO: Add Cleaners to langui */}
<p className="font-headers font-bold">{"Cleaners"}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.cleaners.data, [
"id",
"attributes",
] as const).map((cleaner) => (
<Fragment key={cleaner.id}>
<RecorderChip
langui={langui}
recorder={cleaner.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedScan.typesetters &&
selectedScan.typesetters.data.length > 0 && (
<div>
{/* TODO: Add Cleaners to Typesetters */}
<p className="font-headers font-bold">{"Typesetters"}:</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(selectedScan.typesetters.data, [
"id",
"attributes",
] as const).map((typesetter) => (
<Fragment key={typesetter.id}>
<RecorderChip
langui={langui}
recorder={typesetter.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
</div>
<div
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))] last-of-type:border-0 desktop:grid-cols-[repeat(auto-fill,_minmax(10rem,1fr))]
mobile:grid-cols-2" mobile:grid-cols-2"
> >
{coverImages.map((image, index) => ( {coverImages.map((image, index) => (
<div <div
key={image.url} key={image.url}
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 imgs = coverImages.map((img) => const imgs = coverImages.map((img) =>
getAssetURL(img.url, ImageQuality.Large) getAssetURL(img.url, ImageQuality.Large)
); );
openLightBox(imgs, index); openLightBox(imgs, index);
}} }}
> >
<Img image={image} quality={ImageQuality.Small} /> <Img image={image} quality={ImageQuality.Small} />
</div> </div>
))} ))}
</div>
</div> </div>
)} </div>
</> )}
); </>
} );
return <></>;
}; };

View File

@ -12,7 +12,7 @@ import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { slugify } from "helpers/formatters"; import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
/* /*
@ -31,283 +31,276 @@ export const Markdawn = ({
className, className,
text: rawText, text: rawText,
}: MarkdawnProps): JSX.Element => { }: MarkdawnProps): JSX.Element => {
const appLayout = useAppLayout(); const { playerName } = useAppLayout();
const router = useRouter(); const router = useRouter();
const [openLightBox, LightBox] = useLightBox(); const [openLightBox, LightBox] = useLightBox();
/* eslint-disable no-irregular-whitespace */ /* eslint-disable no-irregular-whitespace */
const text = useMemo( const text = useMemo(
() => `${preprocessMarkDawn(rawText)} () => `${preprocessMarkDawn(rawText, playerName)}
`, `,
[rawText] [playerName, rawText]
); );
/* eslint-enable no-irregular-whitespace */ /* eslint-enable no-irregular-whitespace */
if (text) { if (isUndefined(text) || text === "") {
return ( return <></>;
<> }
<LightBox />
<Markdown return (
className={cJoin("formatted", className)} <>
options={{ <LightBox />
slugify: slugify, <Markdown
overrides: { className={cJoin("formatted", className)}
a: { options={{
component: (compProps: { slugify: slugify,
href: string; overrides: {
children: React.ReactNode; a: {
}) => { component: (compProps: {
if ( href: string;
compProps.href.startsWith("/") || children: React.ReactNode;
compProps.href.startsWith("#") }) => {
) { if (
return ( compProps.href.startsWith("/") ||
<a onClick={async () => router.push(compProps.href)}> compProps.href.startsWith("#")
{compProps.children} ) {
</a>
);
}
return ( return (
<a href={compProps.href} target="_blank" rel="noreferrer"> <a onClick={async () => router.push(compProps.href)}>
{compProps.children} {compProps.children}
</a> </a>
); );
}, }
}, return (
<a href={compProps.href} target="_blank" rel="noreferrer">
h1: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h1 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> </a>
</h1> );
),
}, },
},
h2: { h1: {
component: (compProps: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<h2 id={compProps.id} style={compProps.style}> <h1 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <HeaderToolTip id={compProps.id} />
</h2> </h1>
), ),
}, },
h3: { h2: {
component: (compProps: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<h3 id={compProps.id} style={compProps.style}> <h2 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <HeaderToolTip id={compProps.id} />
</h3> </h2>
), ),
}, },
h4: { h3: {
component: (compProps: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<h4 id={compProps.id} style={compProps.style}> <h3 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <HeaderToolTip id={compProps.id} />
</h4> </h3>
), ),
}, },
h5: { h4: {
component: (compProps: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<h5 id={compProps.id} style={compProps.style}> <h4 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <HeaderToolTip id={compProps.id} />
</h5> </h4>
), ),
}, },
h6: { h5: {
component: (compProps: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: React.ReactNode;
}) => ( }) => (
<h6 id={compProps.id} style={compProps.style}> <h5 id={compProps.id} style={compProps.style}>
{compProps.children} {compProps.children}
<HeaderToolTip id={compProps.id} /> <HeaderToolTip id={compProps.id} />
</h6> </h5>
), ),
}, },
SceneBreak: { h6: {
component: (compProps: { id: string }) => ( component: (compProps: {
<div id: string;
id={compProps.id} style: React.CSSProperties;
className={"mt-16 mb-20 h-0 text-center text-3xl text-dark"} children: React.ReactNode;
> }) => (
* * * <h6 id={compProps.id} style={compProps.style}>
</div> {compProps.children}
), <HeaderToolTip id={compProps.id} />
}, </h6>
),
},
IntraLink: { SceneBreak: {
component: (compProps: { component: (compProps: { id: string }) => (
children: React.ReactNode; <div
target?: string; id={compProps.id}
page?: string; className={"mt-16 mb-20 h-0 text-center text-3xl text-dark"}
}) => { >
const slug = isDefinedAndNotEmpty(compProps.target) * * *
? slugify(compProps.target) </div>
: slugify(compProps.children?.toString()); ),
return ( },
<a
onClick={async () =>
router.replace(`${compProps.page ?? ""}#${slug}`)
}
>
{compProps.children}
</a>
);
},
},
player: { IntraLink: {
component: () => ( component: (compProps: {
<span className="text-dark opacity-70"> children: React.ReactNode;
{appLayout.playerName ?? "<player>"} target?: string;
</span> page?: string;
), }) => {
}, const slug = isDefinedAndNotEmpty(compProps.target)
? slugify(compProps.target)
Transcript: { : slugify(compProps.children?.toString());
component: (compProps) => ( return (
<div className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 mobile:grid-cols-1"> <a
{compProps.children} onClick={async () =>
</div> router.replace(`${compProps.page ?? ""}#${slug}`)
),
},
Line: {
component: (compProps) => (
<>
<strong className="text-dark opacity-60 mobile:!-mb-4">
{compProps.name}
</strong>
<p className="whitespace-pre-line">{compProps.children}</p>
</>
),
},
InsetBox: {
component: (compProps) => (
<InsetBox className="my-12">{compProps.children}</InsetBox>
),
},
li: {
component: (compProps: { children: React.ReactNode }) => (
<li
className={
isDefined(compProps.children) &&
ReactDOMServer.renderToStaticMarkup(
<>{compProps.children}</>
).length > 100
? "my-4"
: ""
} }
> >
{compProps.children} {compProps.children}
</li> </a>
), );
},
Highlight: {
component: (compProps: { children: React.ReactNode }) => (
<mark>{compProps.children}</mark>
),
},
footer: {
component: (compProps: { children: React.ReactNode }) => (
<>
<HorizontalLine />
<div className="grid gap-8">{compProps.children}</div>
</>
),
},
blockquote: {
component: (compProps: {
children: React.ReactNode;
cite?: string;
}) => (
<blockquote>
{isDefinedAndNotEmpty(compProps.cite) ? (
<>
&ldquo;{compProps.children}&rdquo;
<cite> {compProps.cite}</cite>
</>
) : (
compProps.children
)}
</blockquote>
),
},
img: {
component: (compProps: {
alt: string;
src: string;
width?: number;
height?: number;
caption?: string;
name?: string;
}) => (
<div
className="mt-8 mb-12 grid cursor-pointer place-content-center"
onClick={() => {
openLightBox([
compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Large)
: compProps.src,
]);
}}
>
<Img
image={
compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Small)
: compProps.src
}
quality={ImageQuality.Medium}
className="drop-shadow-shade-lg"
></Img>
</div>
),
}, },
}, },
}}
> Transcript: {
{text} component: (compProps) => (
</Markdown> <div className="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 mobile:grid-cols-1">
</> {compProps.children}
); </div>
} ),
return <></>; },
Line: {
component: (compProps) => (
<>
<strong className="text-dark opacity-60 mobile:!-mb-4">
{compProps.name}
</strong>
<p className="whitespace-pre-line">{compProps.children}</p>
</>
),
},
InsetBox: {
component: (compProps) => (
<InsetBox className="my-12">{compProps.children}</InsetBox>
),
},
li: {
component: (compProps: { children: React.ReactNode }) => (
<li
className={
isDefined(compProps.children) &&
ReactDOMServer.renderToStaticMarkup(
<>{compProps.children}</>
).length > 100
? "my-4"
: ""
}
>
{compProps.children}
</li>
),
},
Highlight: {
component: (compProps: { children: React.ReactNode }) => (
<mark>{compProps.children}</mark>
),
},
footer: {
component: (compProps: { children: React.ReactNode }) => (
<>
<HorizontalLine />
<div className="grid gap-8">{compProps.children}</div>
</>
),
},
blockquote: {
component: (compProps: {
children: React.ReactNode;
cite?: string;
}) => (
<blockquote>
{isDefinedAndNotEmpty(compProps.cite) ? (
<>
&ldquo;{compProps.children}&rdquo;
<cite> {compProps.cite}</cite>
</>
) : (
compProps.children
)}
</blockquote>
),
},
img: {
component: (compProps: {
alt: string;
src: string;
width?: number;
height?: number;
caption?: string;
name?: string;
}) => (
<div
className="mt-8 mb-12 grid cursor-pointer place-content-center"
onClick={() => {
openLightBox([
compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Large)
: compProps.src,
]);
}}
>
<Img
image={
compProps.src.startsWith("/uploads/")
? getAssetURL(compProps.src, ImageQuality.Small)
: compProps.src
}
quality={ImageQuality.Medium}
className="drop-shadow-shade-lg"
></Img>
</div>
),
},
},
}}
>
{text}
</Markdown>
</>
);
}; };
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
@ -422,21 +415,6 @@ const HeaderToolTip = (props: { id: string }): JSX.Element => (
* PRIVATE COMPONENTS * PRIVATE COMPONENTS
*/ */
const typographicRules = (text: string): string => {
let newText = text;
newText = newText.replace(/--/gu, "");
/*
* newText = newText.replace(/\.\.\./gu, "");
* newText = newText.replace(/(?:^|[\s{[(<'"\u2018\u201C])(")/gu, " ");
* newText = newText.replace(/"/gu, "");
* newText = newText.replace(/(?:^|[\s{[(<'"\u2018\u201C])(')/gu, " ");
* newText = newText.replace(/'/gu, "");
*/
return newText;
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
enum HeaderLevels { enum HeaderLevels {
H1 = 1, H1 = 1,
H2 = 2, H2 = 2,
@ -446,10 +424,17 @@ enum HeaderLevels {
H6 = 6, H6 = 6,
} }
const preprocessMarkDawn = (text: string): string => { const preprocessMarkDawn = (text: string, playerName = ""): string => {
if (!text) return ""; if (!text) return "";
let preprocessed = typographicRules(text); let preprocessed = text
.replaceAll("--", "—")
.replaceAll(
"@player",
isDefinedAndNotEmpty(playerName) ? playerName : "(player)"
);
console.log();
let scenebreakIndex = 0; let scenebreakIndex = 0;
const visitedSlugs: string[] = []; const visitedSlugs: string[] = [];

View File

@ -1,9 +1,11 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler, useMemo } from "react"; import { MouseEventHandler, useCallback, useMemo } from "react";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { ToolTip } from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { cJoin, cIf } from "helpers/className"; import { cJoin, cIf } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others"; import { isDefinedAndNotEmpty } from "helpers/others";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
/* /*
* *
@ -88,3 +90,45 @@ export const NavOption = ({
</ToolTip> </ToolTip>
); );
}; };
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
interface TranslatedProps extends Omit<Props, "subtitle" | "title"> {
translations: {
title: string | null | undefined;
subtitle?: string | null | undefined;
language: string;
}[];
fallbackTitle: TranslatedProps["translations"][number]["title"];
fallbackSubtitle: TranslatedProps["translations"][number]["subtitle"];
languages: AppStaticProps["languages"];
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TranslatedNavOption = ({
fallbackTitle,
fallbackSubtitle,
translations = [
{ title: fallbackTitle, subtitle: fallbackSubtitle, language: "default" },
],
languages,
...otherProps
}: TranslatedProps): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languages: languages,
languageExtractor: useCallback(
(item: TranslatedProps["translations"][number]) => item.language,
[]
),
});
return (
<NavOption
title={selectedTranslation?.title ?? fallbackTitle}
subtitle={selectedTranslation?.subtitle ?? fallbackSubtitle}
{...otherProps}
/>
);
};

View File

@ -35,7 +35,7 @@ export const ReturnButton = ({
horizontalLine, horizontalLine,
className, className,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const appLayout = useAppLayout(); const { setSubPanelOpen } = useAppLayout();
return ( return (
<div <div
@ -49,7 +49,7 @@ export const ReturnButton = ({
)} )}
> >
<Button <Button
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => setSubPanelOpen(false)}
href={href} href={href}
text={`${langui.return_to} ${title}`} text={`${langui.return_to} ${title}`}
icon={Icon.NavigateBefore} icon={Icon.NavigateBefore}

View File

@ -171,7 +171,11 @@ export const PostPage = ({
description={excerpt} description={excerpt}
langui={langui} langui={langui}
categories={post.categories} categories={post.categories}
languageSwitcher={<LanguageSwitcher {...languageSwitcherProps} />} languageSwitcher={
languageSwitcherProps.locales.size > 1 ? (
<LanguageSwitcher {...languageSwitcherProps} />
) : undefined
}
/> />
<HorizontalLine /> <HorizontalLine />

View File

@ -77,64 +77,61 @@ export const PreviewCard = ({
hoverlay, hoverlay,
infoAppend, infoAppend,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const appLayout = useAppLayout(); const { currency } = useAppLayout();
const metadataJSX = useMemo( const metadataJSX = useMemo(
() => () => (
metadata && (metadata.release_date || metadata.price) ? ( <>
<div className="flex w-full flex-row flex-wrap gap-x-3"> {metadata && (metadata.release_date || metadata.price) && (
{metadata.release_date && ( <div className="flex w-full flex-row flex-wrap gap-x-3">
<p className="text-sm mobile:text-xs"> {metadata.release_date && (
<Ico <p className="text-sm mobile:text-xs">
icon={Icon.Event} <Ico
className="mr-1 translate-y-[.15em] !text-base" icon={Icon.Event}
/> className="mr-1 translate-y-[.15em] !text-base"
{prettyDate(metadata.release_date)} />
</p> {prettyDate(metadata.release_date)}
)} </p>
{metadata.price && metadata.currencies && ( )}
<p className="justify-self-end text-sm mobile:text-xs"> {metadata.price && metadata.currencies && (
<Ico <p className="justify-self-end text-sm mobile:text-xs">
icon={Icon.ShoppingCart} <Ico
className="mr-1 translate-y-[.15em] !text-base" icon={Icon.ShoppingCart}
/> className="mr-1 translate-y-[.15em] !text-base"
{prettyPrice( />
metadata.price, {prettyPrice(metadata.price, metadata.currencies, currency)}
metadata.currencies, </p>
appLayout.currency )}
)} {metadata.views && (
</p> <p className="text-sm mobile:text-xs">
)} <Ico
{metadata.views && ( icon={Icon.Visibility}
<p className="text-sm mobile:text-xs"> className="mr-1 translate-y-[.15em] !text-base"
<Ico />
icon={Icon.Visibility} {prettyShortenNumber(metadata.views)}
className="mr-1 translate-y-[.15em] !text-base" </p>
/> )}
{prettyShortenNumber(metadata.views)} {metadata.author && (
</p> <p className="text-sm mobile:text-xs">
)} <Ico
{metadata.author && ( icon={Icon.Person}
<p className="text-sm mobile:text-xs"> className="mr-1 translate-y-[.15em] !text-base"
<Ico />
icon={Icon.Person} {metadata.author}
className="mr-1 translate-y-[.15em] !text-base" </p>
/> )}
{metadata.author} </div>
</p> )}
)} </>
</div> ),
) : ( [currency, metadata]
<></>
),
[appLayout.currency, metadata]
); );
return ( return (
<Link href={href} passHref> <Link href={href} passHref>
<div <div
className="group grid cursor-pointer items-end transition-transform drop-shadow-shade-xl className="group grid cursor-pointer items-end text-left transition-transform
hover:scale-[1.02]" drop-shadow-shade-xl hover:scale-[1.02]"
> >
{stackNumber > 0 && ( {stackNumber > 0 && (
<> <>

View File

@ -53,6 +53,7 @@ export const SmartList = <T,>({
langui, langui,
}: Props<T>): JSX.Element => { }: Props<T>): JSX.Element => {
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop); useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop);
type Group = Map<string, T[]>; type Group = Map<string, T[]>;
@ -123,62 +124,68 @@ export const SmartList = <T,>({
[paginationItemPerPage, filteredItems.length] [paginationItemPerPage, filteredItems.length]
); );
const changePage = useCallback(
(newPage: number) =>
setPage(() => {
if (newPage <= 0) {
return 0;
}
if (newPage >= pageCount) {
return pageCount;
}
return newPage;
}),
[pageCount]
);
return ( return (
<> <>
{pageCount > 1 && paginationSelectorTop && ( {pageCount > 1 && paginationSelectorTop && (
<PageSelector <PageSelector className="mb-12" page={page} onChange={changePage} />
maxPage={pageCount}
page={page}
setPage={setPage}
className="mb-12"
/>
)} )}
{groupedList.size > 0 <div className="mb-8">
? iterateMap( {groupedList.size > 0
groupedList, ? iterateMap(
(name, groupItems) => groupedList,
groupItems.length > 0 && ( (name, groupItems) =>
<Fragment key={name}> groupItems.length > 0 && (
{name.length > 0 && ( <Fragment key={name}>
<h2 {name.length > 0 && (
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl <h2
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
first-of-type:pt-0" first-of-type:pt-0"
> >
{name} {name}
<Chip <Chip
text={`${groupItems.length} ${ text={`${groupItems.length} ${
groupItems.length <= 1 groupItems.length <= 1
? langui.result?.toLowerCase() ?? "" ? langui.result?.toLowerCase() ?? ""
: langui.results?.toLowerCase() ?? "" : langui.results?.toLowerCase() ?? ""
}`} }`}
/> />
</h2> </h2>
)}
<div
className={cJoin(
`grid items-start gap-8 border-b-[3px] border-dotted pb-12
last-of-type:border-0 mobile:gap-4`,
className
)} )}
> <div
{groupItems.map((item) => ( className={cJoin(
<RenderItem item={item} key={getItemId(item)} /> `grid items-start gap-8 border-b-[3px] border-dotted pb-12
))} last-of-type:border-0 mobile:gap-4`,
</div> className
</Fragment> )}
), >
([a], [b]) => groupSortingFunction(a, b) {groupItems.map((item) => (
) <RenderItem item={item} key={getItemId(item)} />
: isDefined(RenderWhenEmpty) && <RenderWhenEmpty />} ))}
</div>
</Fragment>
),
([a], [b]) => groupSortingFunction(a, b)
)
: isDefined(RenderWhenEmpty) && <RenderWhenEmpty />}
</div>
{pageCount > 1 && paginationSelectorBottom && ( {pageCount > 1 && paginationSelectorBottom && (
<PageSelector <PageSelector className="mb-12" page={page} onChange={changePage} />
maxPage={pageCount}
page={page}
setPage={setPage}
className="mt-12"
/>
)} )}
</> </>
); );

View File

@ -11,6 +11,7 @@ import {
filterDefined, filterDefined,
filterHasAttributes, filterHasAttributes,
getStatusDescription, getStatusDescription,
isDefined,
} from "helpers/others"; } from "helpers/others";
/* /*
@ -30,9 +31,9 @@ export const ChronologyItemComponent = ({
langui, langui,
item, item,
displayYear, displayYear,
}: Props): JSX.Element => { }: Props): JSX.Element => (
if (item.attributes) { <>
return ( {isDefined(item.attributes) && (
<div <div
className="grid grid-cols-[4em] grid-rows-[auto_1fr] place-content-start className="grid grid-cols-[4em] grid-rows-[auto_1fr] place-content-start
rounded-2xl py-4 px-8 target:my-4 target:bg-mid target:py-8" rounded-2xl py-4 px-8 target:my-4 target:bg-mid target:py-8"
@ -100,7 +101,7 @@ export const ChronologyItemComponent = ({
</p> </p>
)} )}
{translation.note ? ( {translation.note ? (
<em>{`Notes: ${translation.note}`}</em> <em>{`${langui.notes}: ${translation.note}`}</em>
) : ( ) : (
"" ""
)} )}
@ -115,7 +116,7 @@ export const ChronologyItemComponent = ({
) : ( ) : (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Ico icon={Icon.Warning} className="!text-sm" /> <Ico icon={Icon.Warning} className="!text-sm" />
No sources! {langui.no_source_warning}
</div> </div>
)} )}
</p> </p>
@ -124,11 +125,9 @@ export const ChronologyItemComponent = ({
))} ))}
</div> </div>
</div> </div>
); )}
} </>
);
return <></>;
};
/* /*
* *

View File

@ -1,10 +1,10 @@
import { useCallback } from "react"; import { useCallback } from "react";
import Link from "next/link";
import { Chip } from "components/Chip"; import { Chip } from "components/Chip";
import { ToolTip } from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getStatusDescription } from "helpers/others"; import { getStatusDescription } from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import Link from "next/link";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
/* /*
@ -88,7 +88,7 @@ const DefinitionCard = ({
{source?.url && source.name && ( {source?.url && source.name && (
<Link href={source.url}> <Link href={source.url}>
<div className="flex place-items-center gap-2 mt-3"> <div className="mt-3 flex place-items-center gap-2">
<p>{langui.source}: </p> <p>{langui.source}: </p>
<Button size="small" text={source.name} /> <Button size="small" text={source.name} />
</div> </div>

View File

@ -102,6 +102,102 @@ query getLibraryItemScans($slug: String, $language_code: String) {
} }
} }
} }
release_date {
...datePicker
}
price {
...pricePicker
}
categories {
data {
id
attributes {
name
short
}
}
}
metadata {
__typename
... on ComponentMetadataBooks {
subtype {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
... on ComponentMetadataGame {
platforms {
data {
id
attributes {
short
}
}
}
}
... on ComponentMetadataVideo {
subtype {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
... on ComponentMetadataAudio {
subtype {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
... on ComponentMetadataGroup {
subtype {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
subitems_type {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
}
}
contents(pagination: { limit: -1 }) { contents(pagination: { limit: -1 }) {
data { data {
id id
@ -122,6 +218,18 @@ query getLibraryItemScans($slug: String, $language_code: String) {
data { data {
attributes { attributes {
slug slug
translations {
pre_title
title
subtitle
language {
data {
attributes {
code
}
}
}
}
} }
} }
} }

View File

@ -159,6 +159,15 @@ query getWebsiteInterface($language_code: String) {
no_results_message no_results_message
all all
special_pages special_pages
scan
scanlation
scanners
cleaners
typesetters
notes
cover
tags
no_source_warning
} }
} }
} }

20
src/hooks/useBoolean.ts Normal file
View File

@ -0,0 +1,20 @@
import { Dispatch, SetStateAction, useCallback, useState } from "react";
export const useBoolean = (
initialState: boolean
): {
state: boolean;
toggleState: () => void;
setTrue: () => void;
setFalse: () => void;
setState: Dispatch<SetStateAction<boolean>>;
} => {
const [state, setState] = useState(initialState);
const toggleState = useCallback(
() => setState((currentState) => !currentState),
[]
);
const setTrue = useCallback(() => setState(true), []);
const setFalse = useCallback(() => setState(false), []);
return { state, toggleState, setTrue, setFalse, setState };
};

View File

@ -15,5 +15,6 @@ export const useScrollTopOnChange = (
document document
.querySelector(`#${id}`) .querySelector(`#${id}`)
?.scrollTo({ top: 0, behavior: "smooth" }); ?.scrollTo({ top: 0, behavior: "smooth" });
}, [id, deps, enabled]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, ...deps, enabled]);
}; };

View File

@ -1,5 +1,6 @@
import Document, { import Document, {
DocumentContext, DocumentContext,
DocumentInitialProps,
Head, Head,
Html, Html,
Main, Main,
@ -7,8 +8,9 @@ import Document, {
} from "next/document"; } from "next/document";
export default class MyDocument extends Document { export default class MyDocument extends Document {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types static async getInitialProps(
static async getInitialProps(ctx: DocumentContext) { ctx: DocumentContext
): Promise<DocumentInitialProps> {
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps }; return { ...initialProps };
} }

View File

@ -1,5 +1,5 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { Fragment, useState, useMemo } from "react"; import { Fragment, useMemo } from "react";
import { AppLayout } from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import { Switch } from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { PanelHeader } from "components/PanelComponents/PanelHeader";
@ -21,6 +21,7 @@ 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 { filterHasAttributes, isDefined } from "helpers/others"; import { filterHasAttributes, isDefined } from "helpers/others";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -34,7 +35,8 @@ interface Props extends AppStaticProps {
} }
const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => { const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => {
const [keepInfoVisible, setKeepInfoVisible] = useState(true); const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } =
useBoolean(true);
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const subPanel = useMemo( const subPanel = useMemo(
@ -58,13 +60,13 @@ const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => {
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
} }
/> />
)} )}
</SubPanel> </SubPanel>
), ),
[hoverable, keepInfoVisible, langui] [hoverable, keepInfoVisible, langui, toggleKeepInfoVisible]
); );
const contentPanel = useMemo( const contentPanel = useMemo(

View File

@ -25,6 +25,7 @@ import { prettyDate } from "helpers/formatters";
import { filterHasAttributes } from "helpers/others"; 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 { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -46,7 +47,10 @@ interface Props extends AppStaticProps {
const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const [keepInfoVisible, setKeepInfoVisible] = useState(true);
const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } =
useBoolean(true);
const [searchName, setSearchName] = useState( const [searchName, setSearchName] = useState(
DEFAULT_FILTERS_STATE.searchName DEFAULT_FILTERS_STATE.searchName
); );
@ -71,21 +75,21 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} value={searchName}
setState={setSearchName} onChange={setSearchName}
/> />
{hoverable && ( {hoverable && (
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
} }
/> />
)} )}
</SubPanel> </SubPanel>
), ),
[hoverable, keepInfoVisible, langui, searchName] [hoverable, keepInfoVisible, langui, searchName, toggleKeepInfoVisible]
); );
const contentPanel = useMemo( const contentPanel = useMemo(

View File

@ -37,7 +37,7 @@ interface Props extends AppStaticProps {
const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => { const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => {
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const appLayout = useAppLayout(); const { setSubPanelOpen } = useAppLayout();
const subPanel = useMemo( const subPanel = useMemo(
() => ( () => (
<SubPanel> <SubPanel>
@ -55,25 +55,25 @@ const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => {
title={langui.video} title={langui.video}
url="#video" url="#video"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => setSubPanelOpen(false)}
/> />
<NavOption <NavOption
title={langui.channel} title={langui.channel}
url="#channel" url="#channel"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => setSubPanelOpen(false)}
/> />
<NavOption <NavOption
title={langui.description} title={langui.description}
url="#description" url="#description"
border border
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => setSubPanelOpen(false)}
/> />
</SubPanel> </SubPanel>
), ),
[appLayout, langui] [setSubPanelOpen, langui]
); );
const contentPanel = useMemo( const contentPanel = useMemo(

View File

@ -206,7 +206,7 @@ const Content = ({
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div> <div>
<p className="font-headers font-bold">{"Notes"}:</p> <p className="font-headers font-bold">{langui.notes}:</p>
<div className="grid place-content-center place-items-center gap-2"> <div className="grid place-content-center place-items-center gap-2">
<Markdawn text={selectedTranslation.text_set.notes} /> <Markdawn text={selectedTranslation.text_set.notes} />
</div> </div>
@ -223,7 +223,7 @@ const Content = ({
<p className="font-headers text-2xl font-bold"> <p className="font-headers text-2xl font-bold">
{langui.source} {langui.source}
</p> </p>
<div className="mt-6 grid place-items-center gap-6 text-left"> <div className="mt-6 grid place-items-center gap-6">
{filterHasAttributes(content.ranged_contents.data, [ {filterHasAttributes(content.ranged_contents.data, [
"attributes.library_item.data.attributes", "attributes.library_item.data.attributes",
"attributes.library_item.data.id", "attributes.library_item.data.id",
@ -330,7 +330,11 @@ const Content = ({
type={content.type} type={content.type}
categories={content.categories} categories={content.categories}
langui={langui} langui={langui}
languageSwitcher={<LanguageSwitcher {...languageSwitcherProps} />} languageSwitcher={
languageSwitcherProps.locales.size > 1 ? (
<LanguageSwitcher {...languageSwitcherProps} />
) : undefined
}
/> />
{previousContent?.attributes && ( {previousContent?.attributes && (

View File

@ -23,6 +23,7 @@ import { GetContentsQuery } from "graphql/generated";
import { SmartList } from "components/SmartList"; import { SmartList } from "components/SmartList";
import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder"; import { ContentPlaceholder } from "components/PanelComponents/ContentPlaceholder";
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -56,9 +57,11 @@ const Contents = ({
const [groupingMethod, setGroupingMethod] = useState<number>( const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod DEFAULT_FILTERS_STATE.groupingMethod
); );
const [keepInfoVisible, setKeepInfoVisible] = useState( const {
DEFAULT_FILTERS_STATE.keepInfoVisible state: keepInfoVisible,
); toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const [combineRelatedContent, setCombineRelatedContent] = useState( const [combineRelatedContent, setCombineRelatedContent] = useState(
DEFAULT_FILTERS_STATE.combineRelatedContent DEFAULT_FILTERS_STATE.combineRelatedContent
); );
@ -149,8 +152,8 @@ const Contents = ({
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} value={searchName}
setState={setSearchName} onChange={setSearchName}
/> />
<WithLabel <WithLabel
@ -159,8 +162,8 @@ const Contents = ({
<Select <Select
className="w-full" className="w-full"
options={[langui.category ?? "", langui.type ?? ""]} options={[langui.category ?? "", langui.type ?? ""]}
state={groupingMethod} value={groupingMethod}
setState={setGroupingMethod} onChange={setGroupingMethod}
allowEmpty allowEmpty
/> />
} }
@ -171,8 +174,8 @@ const Contents = ({
disabled={searchName.length > 1} disabled={searchName.length > 1}
input={ input={
<Switch <Switch
setState={setCombineRelatedContent} value={effectiveCombineRelatedContent}
state={effectiveCombineRelatedContent} onClick={toggleKeepInfoVisible}
/> />
} }
/> />
@ -181,7 +184,7 @@ const Contents = ({
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
} }
/> />
)} )}
@ -208,6 +211,8 @@ const Contents = ({
keepInfoVisible, keepInfoVisible,
langui, langui,
searchName, searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
] ]
); );

View File

@ -54,6 +54,7 @@ import { Ico, Icon } from "components/Ico";
import { cJoin, cIf } from "helpers/className"; import { cJoin, cIf } from "helpers/className";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { getDescription } from "helpers/description"; import { getDescription } from "helpers/description";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -79,10 +80,11 @@ const LibrarySlug = ({
languages, languages,
...otherProps ...otherProps
}: Props): JSX.Element => { }: Props): JSX.Element => {
const appLayout = useAppLayout(); const { currency } = useAppLayout();
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const [openLightBox, LightBox] = useLightBox(); const [openLightBox, LightBox] = useLightBox();
const [keepInfoVisible, setKeepInfoVisible] = useState(false); const { state: keepInfoVisible, toggleState: toggleKeepInfoVisible } =
useBoolean(false);
useScrollTopOnChange(AnchorIds.ContentPanel, [item]); useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
@ -314,14 +316,10 @@ const LibrarySlug = ({
)} )}
</p> </p>
{item.price.currency?.data?.attributes?.code !== {item.price.currency?.data?.attributes?.code !==
appLayout.currency && ( currency && (
<p> <p>
{prettyPrice( {prettyPrice(item.price, currencies, currency)} <br />(
item.price, {langui.calculated?.toLowerCase()})
currencies,
appLayout.currency
)}{" "}
<br />({langui.calculated?.toLowerCase()})
</p> </p>
)} )}
</div> </div>
@ -450,8 +448,8 @@ const LibrarySlug = ({
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch <Switch
setState={setKeepInfoVisible} onClick={toggleKeepInfoVisible}
state={keepInfoVisible} value={keepInfoVisible}
/> />
} }
/> />
@ -572,26 +570,13 @@ const LibrarySlug = ({
[ [
LightBox, LightBox,
langui, langui,
item.thumbnail?.data?.attributes, item,
item.subitem_of?.data,
item.title,
item.subtitle,
item.metadata,
item.descriptions,
item.urls,
item.gallery,
item.release_date,
item.price,
item.categories,
item.size,
item.subitems,
item.contents,
item.slug,
itemId, itemId,
currencies, currencies,
appLayout.currency, currency,
isVariantSet, isVariantSet,
hoverable, hoverable,
toggleKeepInfoVisible,
keepInfoVisible, keepInfoVisible,
displayOpenScans, displayOpenScans,
openLightBox, openLightBox,

View File

@ -1,9 +1,9 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { Fragment, useMemo } from "react"; import { Fragment, useMemo } from "react";
import { AppLayout } from "components/AppLayout"; import { AppLayout } from "components/AppLayout";
import { ScanSet } from "components/Library/ScanSet"; import { TranslatedScanSet } from "components/Library/ScanSet";
import { ScanSetCover } from "components/Library/ScanSetCover"; import { ScanSetCover } from "components/Library/ScanSetCover";
import { NavOption } from "components/PanelComponents/NavOption"; import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { import {
ReturnButton, ReturnButton,
ReturnButtonType, ReturnButtonType,
@ -16,13 +16,21 @@ import { SubPanel } from "components/Panels/SubPanel";
import { GetLibraryItemScansQuery } from "graphql/generated"; 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,
prettyItemSubType,
} from "helpers/formatters";
import { import {
filterHasAttributes, filterHasAttributes,
isDefined, isDefined,
sortRangedContent, sortRangedContent,
} from "helpers/others"; } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { isUntangibleGroupItem } from "helpers/libraryItem";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { PreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
/* /*
* *
@ -42,8 +50,10 @@ interface Props extends AppStaticProps {
const LibrarySlug = ({ const LibrarySlug = ({
item, item,
itemId,
langui, langui,
languages, languages,
currencies,
...otherProps ...otherProps
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [openLightBox, LightBox] = useLightBox(); const [openLightBox, LightBox] = useLightBox();
@ -55,29 +65,109 @@ const LibrarySlug = ({
href={`/library/${item.slug}`} href={`/library/${item.slug}`}
title={langui.item} title={langui.item}
langui={langui} langui={langui}
className="mb-4"
displayOn={ReturnButtonType.Desktop} displayOn={ReturnButtonType.Desktop}
horizontalLine
/> />
{item.contents?.data.map((content) => ( <div className="mobile:w-[80%]">
<NavOption <PreviewCard
key={content.id} href={`/library/${item.slug}`}
url={`#${content.attributes?.slug}`} title={item.title}
title={prettySlug(content.attributes?.slug, item.slug)} subtitle={item.subtitle}
subtitle={ thumbnail={item.thumbnail?.data?.attributes}
content.attributes?.range[0]?.__typename === thumbnailAspectRatio="21/29.7"
"ComponentRangePageRange" thumbnailRounded={false}
? `${content.attributes.range[0].starting_page}` + topChips={
`` + item.metadata && item.metadata.length > 0 && item.metadata[0]
`${content.attributes.range[0].ending_page}` ? [prettyItemSubType(item.metadata[0])]
: undefined : []
}
bottomChips={filterHasAttributes(item.categories?.data, [
"attributes",
] as const).map((category) => category.attributes.short)}
metadata={{
currencies: currencies,
release_date: item.release_date,
price: item.price,
position: "Bottom",
}}
infoAppend={
!isUntangibleGroupItem(item.metadata?.[0]) && (
<PreviewCardCTAs id={itemId} langui={langui} />
)
} }
border
/> />
))} </div>
<HorizontalLine />
<p className="mb-4 font-headers text-2xl font-bold">
{langui.contents}
</p>
{filterHasAttributes(item.contents?.data, ["attributes"] as const).map(
(content) => (
<>
{content.attributes.scan_set &&
content.attributes.scan_set.length > 0 && (
<TranslatedNavOption
key={content.id}
url={`#${content.attributes.slug}`}
translations={filterHasAttributes(
content.attributes.content?.data?.attributes
?.translations,
["language.data.attributes"] as const
).map((translation) => ({
language: translation.language.data.attributes.code,
title: prettyinlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
),
subtitle:
content.attributes.range[0]?.__typename ===
"ComponentRangePageRange"
? `${content.attributes.range[0].starting_page}` +
`` +
`${content.attributes.range[0].ending_page}`
: undefined,
}))}
fallbackTitle={prettySlug(
content.attributes.slug,
item.slug
)}
fallbackSubtitle={
content.attributes.range[0]?.__typename ===
"ComponentRangePageRange"
? `${content.attributes.range[0].starting_page}` +
`` +
`${content.attributes.range[0].ending_page}`
: undefined
}
border
languages={languages}
/>
)}
</>
)
)}
</SubPanel> </SubPanel>
), ),
[item.contents?.data, item.slug, langui] [
currencies,
item.categories?.data,
item.contents?.data,
item.metadata,
item.price,
item.release_date,
item.slug,
item.subtitle,
item.thumbnail?.data?.attributes,
item.title,
itemId,
languages,
langui,
]
); );
const contentPanel = useMemo( const contentPanel = useMemo(
@ -105,11 +195,22 @@ const LibrarySlug = ({
{item.contents?.data.map((content) => ( {item.contents?.data.map((content) => (
<Fragment key={content.id}> <Fragment key={content.id}>
{content.attributes?.scan_set?.[0] && ( {content.attributes?.scan_set?.[0] && (
<ScanSet <TranslatedScanSet
scanSet={content.attributes.scan_set} scanSet={content.attributes.scan_set}
openLightBox={openLightBox} openLightBox={openLightBox}
slug={content.attributes.slug} id={content.attributes.slug}
title={prettySlug(content.attributes.slug, item.slug)} translations={filterHasAttributes(
content.attributes.content?.data?.attributes?.translations,
["language.data.attributes"] as const
).map((translation) => ({
language: translation.language.data.attributes.code,
title: prettyinlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
),
}))}
fallbackTitle={prettySlug(content.attributes.slug, item.slug)}
languages={languages} languages={languages}
langui={langui} langui={langui}
content={content.attributes.content} content={content.attributes.content}
@ -138,6 +239,7 @@ const LibrarySlug = ({
thumbnail={item.thumbnail?.data?.attributes ?? undefined} thumbnail={item.thumbnail?.data?.attributes ?? undefined}
languages={languages} languages={languages}
langui={langui} langui={langui}
currencies={currencies}
{...otherProps} {...otherProps}
/> />
); );

View File

@ -33,6 +33,7 @@ import { useAppLayout } from "contexts/AppLayoutContext";
import { convertPrice } from "helpers/numbers"; import { convertPrice } from "helpers/numbers";
import { SmartList } from "components/SmartList"; import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -66,29 +67,44 @@ const Library = ({
...otherProps ...otherProps
}: Props): JSX.Element => { }: Props): JSX.Element => {
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const appLayout = useAppLayout(); const { libraryItemUserStatus } = useAppLayout();
const [searchName, setSearchName] = useState( const [searchName, setSearchName] = useState(
DEFAULT_FILTERS_STATE.searchName DEFAULT_FILTERS_STATE.searchName
); );
const [showSubitems, setShowSubitems] = useState<boolean>(
DEFAULT_FILTERS_STATE.showSubitems const {
); state: showSubitems,
const [showPrimaryItems, setShowPrimaryItems] = useState<boolean>( toggleState: toggleShowSubitems,
DEFAULT_FILTERS_STATE.showPrimaryItems setState: setShowSubitems,
); } = useBoolean(DEFAULT_FILTERS_STATE.showSubitems);
const [showSecondaryItems, setShowSecondaryItems] = useState<boolean>(
DEFAULT_FILTERS_STATE.showSecondaryItems const {
); state: showPrimaryItems,
toggleState: toggleShowPrimaryItems,
setState: setShowPrimaryItems,
} = useBoolean(DEFAULT_FILTERS_STATE.showPrimaryItems);
const {
state: showSecondaryItems,
toggleState: toggleShowSecondaryItems,
setState: setShowSecondaryItems,
} = useBoolean(DEFAULT_FILTERS_STATE.showSecondaryItems);
const {
state: keepInfoVisible,
toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const [sortingMethod, setSortingMethod] = useState<number>( const [sortingMethod, setSortingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.sortingMethod DEFAULT_FILTERS_STATE.sortingMethod
); );
const [groupingMethod, setGroupingMethod] = useState<number>( const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod DEFAULT_FILTERS_STATE.groupingMethod
); );
const [keepInfoVisible, setKeepInfoVisible] = useState(
DEFAULT_FILTERS_STATE.keepInfoVisible
);
const [filterUserStatus, setFilterUserStatus] = useState< const [filterUserStatus, setFilterUserStatus] = useState<
LibraryItemUserStatus | undefined LibraryItemUserStatus | undefined
>(DEFAULT_FILTERS_STATE.filterUserStatus); >(DEFAULT_FILTERS_STATE.filterUserStatus);
@ -107,28 +123,22 @@ const Library = ({
if (item.attributes.primary && !showPrimaryItems) return false; if (item.attributes.primary && !showPrimaryItems) return false;
if (!item.attributes.primary && !showSecondaryItems) return false; if (!item.attributes.primary && !showSecondaryItems) return false;
if ( if (isDefined(filterUserStatus) && item.id && libraryItemUserStatus) {
isDefined(filterUserStatus) &&
item.id &&
appLayout.libraryItemUserStatus
) {
if (isUntangibleGroupItem(item.attributes.metadata?.[0])) { if (isUntangibleGroupItem(item.attributes.metadata?.[0])) {
return false; return false;
} }
if (filterUserStatus === LibraryItemUserStatus.None) { if (filterUserStatus === LibraryItemUserStatus.None) {
if (appLayout.libraryItemUserStatus[item.id]) { if (libraryItemUserStatus[item.id]) {
return false; return false;
} }
} else if ( } else if (filterUserStatus !== libraryItemUserStatus[item.id]) {
filterUserStatus !== appLayout.libraryItemUserStatus[item.id]
) {
return false; return false;
} }
} }
return true; return true;
}, },
[ [
appLayout.libraryItemUserStatus, libraryItemUserStatus,
filterUserStatus, filterUserStatus,
showPrimaryItems, showPrimaryItems,
showSecondaryItems, showSecondaryItems,
@ -260,8 +270,8 @@ const Library = ({
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} value={searchName}
setState={setSearchName} onChange={setSearchName}
/> />
<WithLabel <WithLabel
@ -274,8 +284,8 @@ const Library = ({
langui.type ?? "Type", langui.type ?? "Type",
langui.release_year ?? "Year", langui.release_year ?? "Year",
]} ]}
state={groupingMethod} value={groupingMethod}
setState={setGroupingMethod} onChange={setGroupingMethod}
allowEmpty allowEmpty
/> />
} }
@ -291,21 +301,21 @@ const Library = ({
langui.price ?? "Price", langui.price ?? "Price",
langui.release_date ?? "Release date", langui.release_date ?? "Release date",
]} ]}
state={sortingMethod} value={sortingMethod}
setState={setSortingMethod} onChange={setSortingMethod}
/> />
} }
/> />
<WithLabel <WithLabel
label={langui.show_subitems} label={langui.show_subitems}
input={<Switch state={showSubitems} setState={setShowSubitems} />} input={<Switch value={showSubitems} onClick={toggleShowSubitems} />}
/> />
<WithLabel <WithLabel
label={langui.show_primary_items} label={langui.show_primary_items}
input={ input={
<Switch state={showPrimaryItems} setState={setShowPrimaryItems} /> <Switch value={showPrimaryItems} onClick={toggleShowPrimaryItems} />
} }
/> />
@ -313,8 +323,8 @@ const Library = ({
label={langui.show_secondary_items} label={langui.show_secondary_items}
input={ input={
<Switch <Switch
state={showSecondaryItems} value={showSecondaryItems}
setState={setShowSecondaryItems} onClick={toggleShowSecondaryItems}
/> />
} }
/> />
@ -323,7 +333,7 @@ const Library = ({
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch state={keepInfoVisible} setState={setKeepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
} }
/> />
)} )}
@ -382,10 +392,18 @@ const Library = ({
keepInfoVisible, keepInfoVisible,
langui, langui,
searchName, searchName,
setKeepInfoVisible,
setShowPrimaryItems,
setShowSecondaryItems,
setShowSubitems,
showPrimaryItems, showPrimaryItems,
showSecondaryItems, showSecondaryItems,
showSubitems, showSubitems,
sortingMethod, sortingMethod,
toggleKeepInfoVisible,
toggleShowPrimaryItems,
toggleShowSecondaryItems,
toggleShowSubitems,
] ]
); );

View File

@ -20,6 +20,7 @@ import { Button } from "components/Inputs/Button";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { filterDefined, filterHasAttributes } from "helpers/others"; import { filterDefined, filterHasAttributes } from "helpers/others";
import { SmartList } from "components/SmartList"; import { SmartList } from "components/SmartList";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -50,9 +51,11 @@ const News = ({
const [searchName, setSearchName] = useState( const [searchName, setSearchName] = useState(
DEFAULT_FILTERS_STATE.searchName DEFAULT_FILTERS_STATE.searchName
); );
const [keepInfoVisible, setKeepInfoVisible] = useState( const {
DEFAULT_FILTERS_STATE.keepInfoVisible state: keepInfoVisible,
); toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const subPanel = useMemo( const subPanel = useMemo(
() => ( () => (
@ -66,15 +69,15 @@ const News = ({
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} value={searchName}
setState={setSearchName} onChange={setSearchName}
/> />
{hoverable && ( {hoverable && (
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
} }
/> />
)} )}
@ -90,7 +93,14 @@ const News = ({
/> />
</SubPanel> </SubPanel>
), ),
[hoverable, keepInfoVisible, langui, searchName] [
hoverable,
keepInfoVisible,
langui,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
); );
const contentPanel = useMemo( const contentPanel = useMemo(

View File

@ -139,11 +139,10 @@ const WikiPage = ({
{page.tags?.data && page.tags.data.length > 0 && ( {page.tags?.data && page.tags.data.length > 0 && (
<> <>
<p className="font-headers text-xl font-bold"> <p className="font-headers text-xl font-bold">
{/* TODO: Add Tags to langui */} {langui.tags}
{"Tags"}
</p> </p>
<div className="flex flex-row flex-wrap place-content-center gap-2"> <div className="flex flex-row flex-wrap place-content-center gap-2">
{filterHasAttributes(page.tags?.data, [ {filterHasAttributes(page.tags.data, [
"attributes", "attributes",
] as const).map((tag) => ( ] as const).map((tag) => (
<Chip <Chip
@ -203,9 +202,11 @@ const WikiPage = ({
), ),
[ [
LanguageSwitcher, LanguageSwitcher,
LightBox,
languageSwitcherProps, languageSwitcherProps,
languages, languages,
langui, langui,
openLightBox,
page, page,
selectedTranslation, selectedTranslation,
] ]

View File

@ -25,6 +25,7 @@ import { SmartList } from "components/SmartList";
import { Select } from "components/Inputs/Select"; import { Select } from "components/Inputs/Select";
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable"; import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { useBoolean } from "hooks/useBoolean";
/* /*
* *
@ -62,9 +63,11 @@ const Wiki = ({
DEFAULT_FILTERS_STATE.groupingMethod DEFAULT_FILTERS_STATE.groupingMethod
); );
const [keepInfoVisible, setKeepInfoVisible] = useState( const {
DEFAULT_FILTERS_STATE.keepInfoVisible state: keepInfoVisible,
); toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const subPanel = useMemo( const subPanel = useMemo(
() => ( () => (
@ -78,8 +81,8 @@ const Wiki = ({
<TextInput <TextInput
className="mb-6 w-full" className="mb-6 w-full"
placeholder={langui.search_title ?? undefined} placeholder={langui.search_title ?? undefined}
state={searchName} value={searchName}
setState={setSearchName} onChange={setSearchName}
/> />
<WithLabel <WithLabel
@ -88,8 +91,8 @@ const Wiki = ({
<Select <Select
className="w-full" className="w-full"
options={[langui.category ?? ""]} options={[langui.category ?? ""]}
state={groupingMethod} value={groupingMethod}
setState={setGroupingMethod} onChange={setGroupingMethod}
allowEmpty allowEmpty
/> />
} }
@ -99,7 +102,7 @@ const Wiki = ({
<WithLabel <WithLabel
label={langui.always_show_info} label={langui.always_show_info}
input={ input={
<Switch setState={setKeepInfoVisible} state={keepInfoVisible} /> <Switch value={keepInfoVisible} onClick={toggleKeepInfoVisible} />
} }
/> />
)} )}
@ -122,7 +125,15 @@ const Wiki = ({
<NavOption title={langui.chronology} url="/wiki/chronology" border /> <NavOption title={langui.chronology} url="/wiki/chronology" border />
</SubPanel> </SubPanel>
), ),
[groupingMethod, hoverable, keepInfoVisible, langui, searchName] [
groupingMethod,
hoverable,
keepInfoVisible,
langui,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
); );
const groupingFunction = useCallback( const groupingFunction = useCallback(