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

View File

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

View File

@ -7,12 +7,14 @@ import { Img } from "components/Img";
import { InsetBox } from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cIf, 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, isUndefined } from "helpers/others"; import { isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { AnchorShare } from "components/AnchorShare"; 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: { component: (compProps: {
id: string; id: string;
style: React.CSSProperties; style: React.CSSProperties;
children: React.ReactNode; children: string;
level: string;
}) => ( }) => (
<h1 id={compProps.id} style={compProps.style}> <Header
{compProps.children} title={compProps.children}
<AnchorShare id={compProps.id} langui={langui} /> langui={langui}
</h1> level={parseInt(compProps.level, 10)}
), slug={compProps.id}
}, />
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>
), ),
}, },
SceneBreak: { SceneBreak: {
component: (compProps: { id: string }) => ( component: (compProps: { id: string }) => (
<div <Header
id={compProps.id} title={"* * *"}
className={"mt-16 mb-20 h-0 text-center text-3xl text-dark"} langui={langui}
> level={6}
* * * slug={compProps.id}
</div> />
), ),
}, },
@ -310,14 +250,14 @@ interface TableOfContentsProps {
text: string; text: string;
title?: string; title?: string;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
horizontalLine?: boolean;
} }
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TableOfContents = ({ export const TableOfContents = ({
text, text,
title, title,
langui, langui,
horizontalLine = false,
}: TableOfContentsProps): JSX.Element => { }: TableOfContentsProps): JSX.Element => {
const router = useRouter(); const router = useRouter();
const toc = useMemo( const toc = useMemo(
@ -327,9 +267,15 @@ export const TableOfContents = ({
return ( return (
<> <>
{toc.children.length > 0 && (
<>
{horizontalLine && <HorizontalLine />}
<h3 className="text-xl">{langui.table_of_contents}</h3> <h3 className="text-xl">{langui.table_of_contents}</h3>
<div className="max-w-[14.5rem] text-left"> <div className="max-w-[14.5rem] text-left">
<p className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap text-left"> <p
className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap
text-left"
>
<a onClick={async () => router.replace(`#${toc.slug}`)}> <a onClick={async () => router.replace(`#${toc.slug}`)}>
{<abbr title={toc.title}>{toc.title}</abbr>} {<abbr title={toc.title}>{toc.title}</abbr>}
</a> </a>
@ -337,6 +283,8 @@ export const TableOfContents = ({
<TocLevel tocchildren={toc.children} parentNumbering="" /> <TocLevel tocchildren={toc.children} parentNumbering="" />
</div> </div>
</> </>
)}
</>
); );
}; };
@ -345,6 +293,80 @@ export const TableOfContents = ({
* PRIVATE COMPONENTS * 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 { interface TocInterface {
title: string; title: string;
slug: string; slug: string;
@ -354,19 +376,35 @@ interface TocInterface {
interface LevelProps { interface LevelProps {
tocchildren: TocInterface[]; tocchildren: TocInterface[];
parentNumbering: string; parentNumbering: string;
allowIntersection?: boolean;
} }
const TocLevel = ({ const TocLevel = ({
tocchildren, tocchildren,
parentNumbering, parentNumbering,
allowIntersection = true,
}: LevelProps): JSX.Element => { }: LevelProps): JSX.Element => {
const router = useRouter(); const router = useRouter();
const ids = useMemo(
() => tocchildren.map((child) => child.slug),
[tocchildren]
);
const currentIntersection = useIntersectionList(ids);
return ( return (
<ol className="pl-4 text-left"> <ol className="pl-4 text-left">
{tocchildren.map((child, childIndex) => ( {tocchildren.map((child, childIndex) => (
<Fragment key={child.slug}> <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}${ <span className="text-dark">{`${parentNumbering}${
childIndex + 1 childIndex + 1
}.`}</span>{" "} }.`}</span>{" "}
@ -377,6 +415,9 @@ const TocLevel = ({
<TocLevel <TocLevel
tocchildren={child.children} tocchildren={child.children}
parentNumbering={`${parentNumbering}${childIndex + 1}.`} parentNumbering={`${parentNumbering}${childIndex + 1}.`}
allowIntersection={
allowIntersection && currentIntersection === childIndex
}
/> />
</Fragment> </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 => { const preprocessMarkDawn = (text: string, playerName = ""): string => {
if (!text) return ""; if (!text) return "";
@ -425,28 +457,8 @@ const preprocessMarkDawn = (text: string, playerName = ""): string => {
return `<SceneBreak id="scene-break-${scenebreakIndex}">`; return `<SceneBreak id="scene-break-${scenebreakIndex}">`;
} }
if (line.startsWith("# ")) { if (/^[#]+ /u.test(line)) {
return markdawnHeadersParser(HeaderLevels.H1, line, visitedSlugs); return markdawnHeadersParser(line.indexOf(" "), 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);
} }
return line; return line;
@ -459,7 +471,7 @@ const preprocessMarkDawn = (text: string, playerName = ""): string => {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const markdawnHeadersParser = ( const markdawnHeadersParser = (
headerLevel: HeaderLevels, headerLevel: number,
line: string, line: string,
visitedSlugs: string[] visitedSlugs: string[]
): string => { ): string => {
@ -472,7 +484,7 @@ const markdawnHeadersParser = (
index++; index++;
} }
visitedSlugs.push(newSlug); 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(`">`)); line.slice(line.indexOf(`id="`) + 4, line.indexOf(`">`));
text.split("\n").map((line) => { text.split("\n").map((line) => {
if (line.startsWith("<h1 id=")) { if (line.startsWith('<Header level="2"')) {
toc.title = getTitle(line);
toc.slug = getSlug(line);
} else if (line.startsWith("<h2 id=")) {
toc.children.push({ toc.children.push({
title: getTitle(line), title: getTitle(line),
slug: getSlug(line), slug: getSlug(line),
@ -511,7 +520,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4 = -1; h4 = -1;
h5 = -1; h5 = -1;
scenebreak = 0; scenebreak = 0;
} else if (h2 >= 0 && line.startsWith("<h3 id=")) { } else if (h2 >= 0 && line.startsWith('<Header level="3"')) {
toc.children[h2].children.push({ toc.children[h2].children.push({
title: getTitle(line), title: getTitle(line),
slug: getSlug(line), slug: getSlug(line),
@ -521,7 +530,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4 = -1; h4 = -1;
h5 = -1; h5 = -1;
scenebreak = 0; 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({ toc.children[h2].children[h3].children.push({
title: getTitle(line), title: getTitle(line),
slug: getSlug(line), slug: getSlug(line),
@ -530,7 +539,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
h4++; h4++;
h5 = -1; h5 = -1;
scenebreak = 0; 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({ toc.children[h2].children[h3].children[h4].children.push({
title: getTitle(line), title: getTitle(line),
slug: getSlug(line), slug: getSlug(line),
@ -538,7 +547,7 @@ const getTocFromMarkdawn = (text: string, title?: string): TocInterface => {
}); });
h5++; h5++;
scenebreak = 0; 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({ toc.children[h2].children[h3].children[h4].children[h5].children.push({
title: getTitle(line), title: getTitle(line),
slug: getSlug(line), slug: getSlug(line),

View File

@ -20,6 +20,7 @@ interface Props {
subtitle?: string | null | undefined; subtitle?: string | null | undefined;
border?: boolean; border?: boolean;
reduced?: boolean; reduced?: boolean;
active?: boolean;
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
} }
@ -32,12 +33,13 @@ export const NavOption = ({
subtitle, subtitle,
border = false, border = false,
reduced = false, reduced = false,
active = false,
onClick, onClick,
}: Props): JSX.Element => { }: Props): JSX.Element => {
const router = useRouter(); const router = useRouter();
const isActive = useMemo( const isActive = useMemo(
() => router.asPath.startsWith(url), () => active || router.asPath.startsWith(url),
[url, router.asPath] [active, router.asPath, url]
); );
return ( 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"> <div className="grid grid-flow-col place-content-start place-items-center gap-6">
{recorder.avatar?.data?.attributes && ( {recorder.avatar?.data?.attributes && (
<Img <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} src={recorder.avatar.data.attributes}
quality={ImageQuality.Small} 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 { export enum AnchorIds {
ContentPanel = "contentPanel495922447721572", ContentPanel = "contentPanel495922447721572",
@ -18,3 +19,23 @@ export const useScrollTopOnChange = (
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, ...deps, enabled]); }, [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(() => { useEffect(() => {
setSelectedTranslationIndex( setSelectedTranslationIndex(
getPreferredLanguage( getPreferredLanguage(preferredLanguages, availableLocales)
preferredLanguages,
availableLocales
)
); );
}, [preferredLanguages, availableLocales, router.locale]); }, [preferredLanguages, availableLocales, router.locale]);

View File

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

View File

@ -123,11 +123,12 @@ const Content = ({
<TranslatedReturnButton <TranslatedReturnButton
{...returnButtonProps} {...returnButtonProps}
displayOn={ReturnButtonType.Desktop} displayOn={ReturnButtonType.Desktop}
horizontalLine
/> />
{selectedTranslation?.text_set?.source_language?.data?.attributes {selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && ( ?.code !== undefined && (
<>
<HorizontalLine />
<div className="grid gap-5"> <div className="grid gap-5">
<h2 className="text-xl"> <h2 className="text-xl">
{selectedTranslation.text_set.source_language.data.attributes {selectedTranslation.text_set.source_language.data.attributes
@ -145,8 +146,8 @@ const Content = ({
</p> </p>
<Chip <Chip
text={prettyLanguage( text={prettyLanguage(
selectedTranslation.text_set.source_language.data.attributes selectedTranslation.text_set.source_language.data
.code, .attributes.code,
languages languages
)} )}
/> />
@ -245,6 +246,22 @@ const Content = ({
</div> </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 && {content.ranged_contents?.data &&
@ -315,21 +332,6 @@ const Content = ({
</div> </div>
</> </>
)} )}
{selectedTranslation?.text_set?.text && (
<>
<HorizontalLine />
<TableOfContents
text={selectedTranslation.text_set.text}
title={prettyInlineTitle(
selectedTranslation.pre_title,
selectedTranslation.title,
selectedTranslation.subtitle
)}
langui={langui}
/>
</>
)}
</SubPanel> </SubPanel>
), ),
[ [

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
@tailwind utilities; @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, h1,