Added intersection for improved UX on page navigation

This commit is contained in:
DrMint 2022-08-21 02:46:44 +02:00
parent b7ebda4f4f
commit e947fd7a0e
14 changed files with 447 additions and 286 deletions

View File

@ -1,6 +1,7 @@
import { Ico, Icon } from "./Ico";
import { ToolTip } from "./ToolTip";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className";
/*
*
@ -10,11 +11,12 @@ import { AppStaticProps } from "graphql/getAppStaticProps";
interface Props {
id: string;
langui: AppStaticProps["langui"];
className?: string;
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const AnchorShare = ({ id, langui }: Props): JSX.Element => (
export const AnchorShare = ({ id, langui, className }: Props): JSX.Element => (
<ToolTip
content={langui.copy_anchor_link}
trigger="mouseenter"
@ -27,7 +29,10 @@ export const AnchorShare = ({ id, langui }: Props): JSX.Element => (
>
<Ico
icon={Icon.Link}
className="transition-color cursor-pointer hover:text-dark"
className={cJoin(
"transition-color cursor-pointer hover:text-dark",
className
)}
onClick={() => {
navigator.clipboard.writeText(
`${

View File

@ -64,7 +64,6 @@ const ChroniclesList = ({
<div
key={chronicle.id}
id={`chronicle-${chronicle.attributes.slug}`}
className="scroll-m-[45vh]"
>
{chronicle.attributes.translations.length === 0 &&
chronicle.attributes.contents.data.length === 1

View File

@ -7,12 +7,14 @@ import { Img } from "components/Img";
import { InsetBox } from "components/InsetBox";
import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className";
import { cIf, cJoin } from "helpers/className";
import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { useLightBox } from "hooks/useLightBox";
import { AnchorShare } from "components/AnchorShare";
import { useIntersectionList } from "hooks/useIntersectionList";
import { Ico, Icon } from "components/Ico";
/*
*
@ -79,92 +81,30 @@ export const Markdawn = ({
},
},
h1: {
Header: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
children: string;
level: string;
}) => (
<h1 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h1>
),
},
h2: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h2 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h2>
),
},
h3: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h3 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h3>
),
},
h4: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h4 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h4>
),
},
h5: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h5 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h5>
),
},
h6: {
component: (compProps: {
id: string;
style: React.CSSProperties;
children: React.ReactNode;
}) => (
<h6 id={compProps.id} style={compProps.style}>
{compProps.children}
<AnchorShare id={compProps.id} langui={langui} />
</h6>
<Header
title={compProps.children}
langui={langui}
level={parseInt(compProps.level, 10)}
slug={compProps.id}
/>
),
},
SceneBreak: {
component: (compProps: { id: string }) => (
<div
id={compProps.id}
className={"mt-16 mb-20 h-0 text-center text-3xl text-dark"}
>
* * *
</div>
<Header
title={"* * *"}
langui={langui}
level={6}
slug={compProps.id}
/>
),
},
@ -310,14 +250,14 @@ interface TableOfContentsProps {
text: string;
title?: string;
langui: AppStaticProps["langui"];
horizontalLine?: boolean;
}
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TableOfContents = ({
text,
title,
langui,
horizontalLine = false,
}: TableOfContentsProps): JSX.Element => {
const router = useRouter();
const toc = useMemo(
@ -327,15 +267,23 @@ export const TableOfContents = ({
return (
<>
<h3 className="text-xl">{langui.table_of_contents}</h3>
<div className="max-w-[14.5rem] text-left">
<p className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap text-left">
<a onClick={async () => router.replace(`#${toc.slug}`)}>
{<abbr title={toc.title}>{toc.title}</abbr>}
</a>
</p>
<TocLevel tocchildren={toc.children} parentNumbering="" />
</div>
{toc.children.length > 0 && (
<>
{horizontalLine && <HorizontalLine />}
<h3 className="text-xl">{langui.table_of_contents}</h3>
<div className="max-w-[14.5rem] text-left">
<p
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
text-left"
>
<a onClick={async () => router.replace(`#${toc.slug}`)}>
{<abbr title={toc.title}>{toc.title}</abbr>}
</a>
</p>
<TocLevel tocchildren={toc.children} parentNumbering="" />
</div>
</>
)}
</>
);
};
@ -345,6 +293,80 @@ export const TableOfContents = ({
* PRIVATE COMPONENTS
*/
interface HeaderProps {
level: number;
title: string;
slug: string;
langui: AppStaticProps["langui"];
}
const Header = ({ level, title, slug, langui }: HeaderProps): JSX.Element => {
const innerComponent = useMemo(
() => (
<>
<div className="mt-8 mr-2 mb-12 flex place-items-center gap-4">
{title === "* * *" ? (
<div className="space-x-3 text-dark">
<Ico icon={Icon.Emergency} />
<Ico icon={Icon.Emergency} />
<Ico icon={Icon.Emergency} />
</div>
) : (
<div className="font-headers">{title}</div>
)}
<AnchorShare
className="opacity-0 transition-opacity group-hover:opacity-100"
id={slug}
langui={langui}
/>
</div>
</>
),
[langui, slug, title]
);
const className = "group";
switch (level) {
case 1:
return (
<h1 id={slug} className={className}>
{innerComponent}
</h1>
);
case 2:
return (
<h2 id={slug} className={className}>
{innerComponent}
</h2>
);
case 3:
return (
<h3 id={slug} className={className}>
{innerComponent}
</h3>
);
case 4:
return (
<h4 id={slug} className={className}>
{innerComponent}
</h4>
);
case 5:
return (
<h5 id={slug} className={className}>
{innerComponent}
</h5>
);
default:
return (
<h6 id={slug} className={className}>
{innerComponent}
</h6>
);
}
};
interface TocInterface {
title: string;
slug: string;
@ -354,19 +376,35 @@ interface TocInterface {
interface LevelProps {
tocchildren: TocInterface[];
parentNumbering: string;
allowIntersection?: boolean;
}
const TocLevel = ({
tocchildren,
parentNumbering,
allowIntersection = true,
}: LevelProps): JSX.Element => {
const router = useRouter();
const ids = useMemo(
() => tocchildren.map((child) => child.slug),
[tocchildren]
);
const currentIntersection = useIntersectionList(ids);
return (
<ol className="pl-4 text-left">
{tocchildren.map((child, childIndex) => (
<Fragment key={child.slug}>
<li className="my-2 w-full overflow-x-hidden text-ellipsis whitespace-nowrap">
<li
className={cJoin(
"my-2 w-full overflow-x-hidden text-ellipsis whitespace-nowrap",
cIf(
allowIntersection && currentIntersection === childIndex,
"text-dark"
)
)}
>
<span className="text-dark">{`${parentNumbering}${
childIndex + 1
}.`}</span>{" "}
@ -377,6 +415,9 @@ const TocLevel = ({
<TocLevel
tocchildren={child.children}
parentNumbering={`${parentNumbering}${childIndex + 1}.`}
allowIntersection={
allowIntersection && currentIntersection === childIndex
}
/>
</Fragment>
))}
@ -385,19 +426,10 @@ const TocLevel = ({
};
/*
*
* PRIVATE COMPONENTS
*
* PRIVATE METHODS
*/
enum HeaderLevels {
H1 = 1,
H2 = 2,
H3 = 3,
H4 = 4,
H5 = 5,
H6 = 6,
}
const preprocessMarkDawn = (text: string, playerName = ""): string => {
if (!text) return "";
@ -425,28 +457,8 @@ const preprocessMarkDawn = (text: string, playerName = ""): string => {
return `<SceneBreak id="scene-break-${scenebreakIndex}">`;
}
if (line.startsWith("# ")) {
return markdawnHeadersParser(HeaderLevels.H1, line, visitedSlugs);
}
if (line.startsWith("## ")) {
return markdawnHeadersParser(HeaderLevels.H2, line, visitedSlugs);
}
if (line.startsWith("### ")) {
return markdawnHeadersParser(HeaderLevels.H3, line, visitedSlugs);
}
if (line.startsWith("#### ")) {
return markdawnHeadersParser(HeaderLevels.H4, line, visitedSlugs);
}
if (line.startsWith("##### ")) {
return markdawnHeadersParser(HeaderLevels.H5, line, visitedSlugs);
}
if (line.startsWith("###### ")) {
return markdawnHeadersParser(HeaderLevels.H6, line, visitedSlugs);
if (/^[#]+ /u.test(line)) {
return markdawnHeadersParser(line.indexOf(" "), line, visitedSlugs);
}
return line;
@ -459,7 +471,7 @@ const preprocessMarkDawn = (text: string, playerName = ""): string => {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const markdawnHeadersParser = (
headerLevel: HeaderLevels,
headerLevel: number,
line: string,
visitedSlugs: string[]
): string => {
@ -472,7 +484,7 @@ const markdawnHeadersParser = (
index++;
}
visitedSlugs.push(newSlug);
return `<h${headerLevel} id="${newSlug}">${lineText}</h${headerLevel}>`;
return `<Header level="${headerLevel}" id="${newSlug}">${lineText}</Header>`;
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
@ -497,10 +509,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
line.slice(line.indexOf(`id="`) + 4, line.indexOf(`">`));
text.split("\n").map((line) => {
if (line.startsWith("<h1 id=")) {
toc.title = getTitle(line);
toc.slug = getSlug(line);
} else if (line.startsWith("<h2 id=")) {
if (line.startsWith('<Header level="2"')) {
toc.children.push({
title: getTitle(line),
slug: getSlug(line),
@ -511,7 +520,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4 = -1;
h5 = -1;
scenebreak = 0;
} else if (h2 >= 0 && line.startsWith("<h3 id=")) {
} else if (h2 >= 0 && line.startsWith('<Header level="3"')) {
toc.children[h2].children.push({
title: getTitle(line),
slug: getSlug(line),
@ -521,7 +530,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4 = -1;
h5 = -1;
scenebreak = 0;
} else if (h3 >= 0 && line.startsWith("<h4 id=")) {
} else if (h3 >= 0 && line.startsWith('<Header level="4"')) {
toc.children[h2].children[h3].children.push({
title: getTitle(line),
slug: getSlug(line),
@ -530,7 +539,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4++;
h5 = -1;
scenebreak = 0;
} else if (h4 >= 0 && line.startsWith("<h5 id=")) {
} else if (h4 >= 0 && line.startsWith('<Header level="5"')) {
toc.children[h2].children[h3].children[h4].children.push({
title: getTitle(line),
slug: getSlug(line),
@ -538,7 +547,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
});
h5++;
scenebreak = 0;
} else if (h5 >= 0 && line.startsWith("<h6 id=")) {
} else if (h5 >= 0 && line.startsWith('<Header level="6"')) {
toc.children[h2].children[h3].children[h4].children[h5].children.push({
title: getTitle(line),
slug: getSlug(line),

View File

@ -20,6 +20,7 @@ interface Props {
subtitle?: string | null | undefined;
border?: boolean;
reduced?: boolean;
active?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>;
}
@ -32,12 +33,13 @@ export const NavOption = ({
subtitle,
border = false,
reduced = false,
active = false,
onClick,
}: Props): JSX.Element => {
const router = useRouter();
const isActive = useMemo(
() => router.asPath.startsWith(url),
[url, router.asPath]
() => active || router.asPath.startsWith(url),
[active, router.asPath, url]
);
return (

View File

@ -28,7 +28,7 @@ export const RecorderChip = ({ recorder, langui }: Props): JSX.Element => (
<div className="grid grid-flow-col place-content-start place-items-center gap-6">
{recorder.avatar?.data?.attributes && (
<Img
className="w-20 rounded-full border-4 border-mid aspect-square object-cover"
className="aspect-square w-20 rounded-full border-4 border-mid object-cover"
src={recorder.avatar.data.attributes}
quality={ImageQuality.Small}
/>

View File

@ -0,0 +1,51 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { throttle } from "throttle-debounce";
import { useIsClient } from "usehooks-ts";
import { useOnScroll, AnchorIds } from "./useScrollTopOnChange";
import { isDefined } from "helpers/others";
export const useIntersectionList = (ids: string[]): number => {
const [currentIntersection, setCurrentIntersection] = useState(-1);
const isClient = useIsClient();
const contentPanel = useMemo(
() => (isClient ? document.getElementById(AnchorIds.ContentPanel) : null),
[isClient]
);
const refreshCurrentIntersection = useCallback(
(scroll: number) => {
console.log("update");
if (!isDefined(contentPanel)) {
setCurrentIntersection(-1);
return;
}
for (let idIndex = 0; idIndex < ids.length; idIndex++) {
const elem = document.getElementById(ids[ids.length - 1 - idIndex]);
const halfScreenOffset = window.screen.height / 2;
if (isDefined(elem) && scroll > elem.offsetTop - halfScreenOffset) {
setCurrentIntersection(ids.length - 1 - idIndex);
return;
}
}
setCurrentIntersection(-1);
},
[ids, contentPanel]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledRefreshCurrentIntersection = useCallback(
throttle(100, refreshCurrentIntersection),
[refreshCurrentIntersection]
);
useOnScroll(AnchorIds.ContentPanel, throttledRefreshCurrentIntersection);
useEffect(() => refreshCurrentIntersection(0), [refreshCurrentIntersection]);
return currentIntersection;
};

View File

@ -1,4 +1,5 @@
import { DependencyList, useEffect } from "react";
import { DependencyList, useCallback, useEffect, useMemo } from "react";
import { useIsClient } from "usehooks-ts";
export enum AnchorIds {
ContentPanel = "contentPanel495922447721572",
@ -18,3 +19,23 @@ export const useScrollTopOnChange = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, ...deps, enabled]);
};
export const useOnScroll = (
id: AnchorIds,
onScroll: (scroll: number) => void
): void => {
const isClient = useIsClient();
const elem = useMemo(
() => (isClient ? document.querySelector(`#${id}`) : null),
[id, isClient]
);
const listener = useCallback(() => {
if (elem?.scrollTop) {
onScroll(elem.scrollTop);
}
}, [elem?.scrollTop, onScroll]);
useEffect(() => {
elem?.addEventListener("scroll", listener);
return () => elem?.removeEventListener("scrool", listener);
}, [elem, listener]);
};

View File

@ -41,10 +41,7 @@ export const useSmartLanguage = <T>({
useEffect(() => {
setSelectedTranslationIndex(
getPreferredLanguage(
preferredLanguages,
availableLocales
)
getPreferredLanguage(preferredLanguages, availableLocales)
);
}, [preferredLanguages, availableLocales, router.locale]);

View File

@ -161,6 +161,7 @@ const Revalidate = (
case "content": {
paths.push(`/contents`);
paths.push(`/contents/all`);
paths.push(`/contents/${body.entry.slug}`);
if (body.entry.folder?.slug) {
paths.push(`/contents/folder/${body.entry.folder.slug}`);

View File

@ -123,128 +123,145 @@ const Content = ({
<TranslatedReturnButton
{...returnButtonProps}
displayOn={ReturnButtonType.Desktop}
horizontalLine
/>
{selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && (
<div className="grid gap-5">
<h2 className="text-xl">
<>
<HorizontalLine />
<div className="grid gap-5">
<h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes
.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2>
{selectedTranslation.text_set.source_language.data.attributes
.code === selectedTranslation.language?.data?.attributes?.code
? langui.transcript_notice
: langui.translation_notice}
</h2>
{selectedTranslation.text_set.source_language.data.attributes
.code !==
selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2">
<p className="font-headers font-bold">
{langui.source_language}:
</p>
<Chip
text={prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes
.code,
languages
)}
/>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.text_set.status,
langui
)}
maxWidth={"20rem"}
>
<Chip text={selectedTranslation.text_set.status} />
</ToolTip>
</div>
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
.code !==
selectedTranslation.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2">
<p className="font-headers font-bold">
{langui.transcribers}:
{langui.source_language}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.transcribers.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers font-bold">
{langui.translators}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.translators.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers font-bold">
{langui.proofreaders}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.proofreaders.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers font-bold">{langui.notes}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn
text={selectedTranslation.text_set.notes}
langui={langui}
<Chip
text={prettyLanguage(
selectedTranslation.text_set.source_language.data
.attributes.code,
languages
)}
/>
</div>
)}
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
<p className="font-headers font-bold">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.text_set.status,
langui
)}
maxWidth={"20rem"}
>
<Chip text={selectedTranslation.text_set.status} />
</ToolTip>
</div>
)}
</div>
{selectedTranslation.text_set.transcribers &&
selectedTranslation.text_set.transcribers.data.length > 0 && (
<div>
<p className="font-headers font-bold">
{langui.transcribers}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.transcribers.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.translators &&
selectedTranslation.text_set.translators.data.length > 0 && (
<div>
<p className="font-headers font-bold">
{langui.translators}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.translators.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{selectedTranslation.text_set.proofreaders &&
selectedTranslation.text_set.proofreaders.data.length > 0 && (
<div>
<p className="font-headers font-bold">
{langui.proofreaders}:
</p>
<div className="grid place-content-center place-items-center gap-2">
{filterHasAttributes(
selectedTranslation.text_set.proofreaders.data,
["attributes", "id"] as const
).map((recorder) => (
<Fragment key={recorder.id}>
<RecorderChip
langui={langui}
recorder={recorder.attributes}
/>
</Fragment>
))}
</div>
</div>
)}
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
<div>
<p className="font-headers font-bold">{langui.notes}:</p>
<div className="grid place-content-center place-items-center gap-2">
<Markdawn
text={selectedTranslation.text_set.notes}
langui={langui}
/>
</div>
</div>
)}
</div>
</>
)}
{selectedTranslation?.text_set?.text && (
<>
<TableOfContents
text={selectedTranslation.text_set.text}
title={prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
langui={langui}
horizontalLine
/>
</>
)}
{content.ranged_contents?.data &&
@ -315,21 +332,6 @@ const Content = ({
</div>
</>
)}
{selectedTranslation?.text_set?.text && (
<>
<HorizontalLine />
<TableOfContents
text={selectedTranslation.text_set.text}
title={prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
langui={langui}
/>
</>
)}
</SubPanel>
),
[

View File

@ -56,6 +56,20 @@ import { cJoin, cIf } from "helpers/className";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { getOpenGraph } from "helpers/openGraph";
import { getDescription } from "helpers/description";
import { useIntersectionList } from "hooks/useIntersectionList";
/*
*
* CONSTANTS
*/
const intersectionIds = [
"summary",
"gallery",
"details",
"subitems",
"contents",
];
/*
*
@ -90,6 +104,8 @@ const LibrarySlug = ({
useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
const currentIntersection = useIntersectionList(intersectionIds);
const isVariantSet = useMemo(
() =>
item.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
@ -118,29 +134,57 @@ const LibrarySlug = ({
/>
<div className="grid gap-4">
<NavOption title={langui.summary} url="#summary" border />
<NavOption
title={langui.summary}
url={`#${intersectionIds[0]}`}
border
active={currentIntersection === 0}
/>
{item.gallery && item.gallery.data.length > 0 && (
<NavOption title={langui.gallery} url="#gallery" border />
<NavOption
title={langui.gallery}
url={`#${intersectionIds[1]}`}
border
active={currentIntersection === 1}
/>
)}
<NavOption title={langui.details} url="#details" border />
<NavOption
title={langui.details}
url={`#${intersectionIds[2]}`}
border
active={currentIntersection === 2}
/>
{item.subitems && item.subitems.data.length > 0 && (
<NavOption
title={isVariantSet ? langui.variants : langui.subitems}
url={isVariantSet ? "#variants" : "#subitems"}
url={`#${intersectionIds[3]}`}
border
active={currentIntersection === 3}
/>
)}
{item.contents && item.contents.data.length > 0 && (
<NavOption title={langui.contents} url="#contents" border />
<NavOption
title={langui.contents}
url={`#${intersectionIds[4]}`}
border
active={currentIntersection === 4}
/>
)}
</div>
</SubPanel>
),
[isVariantSet, item.contents, item.gallery, item.subitems, langui]
[
currentIntersection,
isVariantSet,
item.contents,
item.gallery,
item.subitems,
langui,
]
);
const contentPanel = useMemo(
@ -181,7 +225,7 @@ const LibrarySlug = ({
)}
</div>
<InsetBox id="summary" className="grid place-items-center">
<InsetBox id={intersectionIds[0]} className="grid place-items-center">
<div className="grid w-[clamp(0px,100%,42rem)] place-items-center gap-8">
{item.subitem_of?.data[0]?.attributes && (
<div className="grid place-items-center">
@ -246,7 +290,10 @@ const LibrarySlug = ({
</InsetBox>
{item.gallery && item.gallery.data.length > 0 && (
<div id="gallery" className="grid w-full place-items-center gap-8">
<div
id={intersectionIds[1]}
className="grid w-full place-items-center gap-8"
>
<h2 className="text-2xl">{langui.gallery}</h2>
<div
className="grid w-full grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] items-end
@ -282,7 +329,7 @@ const LibrarySlug = ({
</div>
)}
<InsetBox id="details" className="grid place-items-center">
<InsetBox id={intersectionIds[2]} className="grid place-items-center">
<div className="place-items grid w-[clamp(0px,100%,42rem)] gap-8">
<h2 className="text-center text-2xl">{langui.details}</h2>
<div
@ -438,7 +485,7 @@ const LibrarySlug = ({
{item.subitems && item.subitems.data.length > 0 && (
<div
id={isVariantSet ? "variants" : "subitems"}
id={intersectionIds[3]}
className="grid w-full place-items-center gap-8"
>
<h2 className="text-2xl">
@ -500,7 +547,10 @@ const LibrarySlug = ({
)}
{item.contents && item.contents.data.length > 0 && (
<div id="contents" className="grid w-full place-items-center gap-8">
<div
id={intersectionIds[4]}
className="grid w-full place-items-center gap-8"
>
<h2 className="-mb-6 text-2xl">{langui.contents}</h2>
{displayOpenScans && (
<Button

View File

@ -44,6 +44,7 @@ import { isInteger } from "helpers/numbers";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { TranslatedProps } from "helpers/types/TranslatedProps";
import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { useIntersectionList } from "hooks/useIntersectionList";
/*
*
@ -71,6 +72,15 @@ const LibrarySlug = ({
}: Props): JSX.Element => {
const [openLightBox, LightBox] = useLightBox();
const ids = useMemo(
() =>
filterHasAttributes(item.contents?.data, [
"attributes.slug",
] as const).map((content) => content.attributes.slug),
[item.contents?.data]
);
const currentIntersection = useIntersectionList(ids);
const subPanel = useMemo(
() => (
<SubPanel>
@ -121,7 +131,7 @@ const LibrarySlug = ({
</p>
{filterHasAttributes(item.contents?.data, ["attributes"] as const).map(
(content) => (
(content, index) => (
<>
{content.attributes.scan_set &&
content.attributes.scan_set.length > 0 && (
@ -158,6 +168,7 @@ const LibrarySlug = ({
: undefined,
}}
border
active={index === currentIntersection}
/>
)}
</>
@ -167,6 +178,7 @@ const LibrarySlug = ({
),
[
currencies,
currentIntersection,
item.categories?.data,
item.contents?.data,
item.metadata,

View File

@ -32,6 +32,7 @@ import { AnchorShare } from "components/AnchorShare";
import { datePickerToDate } from "helpers/date";
import { TranslatedProps } from "helpers/types/TranslatedProps";
import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { useIntersectionList } from "hooks/useIntersectionList";
/*
*
@ -52,6 +53,16 @@ const Chronology = ({
languages,
...otherProps
}: Props): JSX.Element => {
const ids = useMemo(
() =>
filterHasAttributes(chronologyEras, ["attributes"] as const).map(
(era) => era.attributes.slug
),
[chronologyEras]
);
const currentIntersection = useIntersectionList(ids);
const subPanel = useMemo(
() => (
<SubPanel>
@ -64,7 +75,7 @@ const Chronology = ({
/>
{filterHasAttributes(chronologyEras, ["attributes", "id"] as const).map(
(era) => (
(era, index) => (
<Fragment key={era.id}>
<TranslatedNavOption
translations={filterHasAttributes(era.attributes.title, [
@ -80,13 +91,14 @@ const Chronology = ({
}}
url={`#${era.attributes.slug}`}
border
active={currentIntersection === index}
/>
</Fragment>
)
)}
</SubPanel>
),
[chronologyEras, langui]
[chronologyEras, currentIntersection, langui]
);
const contentPanel = useMemo(

View File

@ -3,7 +3,7 @@
@tailwind utilities;
* {
@apply box-border scroll-m-8 scroll-smooth font-body font-medium;
@apply box-border scroll-m-[40vh] scroll-smooth font-body font-medium;
}
h1,