Added intersection for improved UX on page navigation
This commit is contained in:
parent
b7ebda4f4f
commit
e947fd7a0e
|
@ -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(
|
||||
`${
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -41,10 +41,7 @@ export const useSmartLanguage = <T>({
|
|||
|
||||
useEffect(() => {
|
||||
setSelectedTranslationIndex(
|
||||
getPreferredLanguage(
|
||||
preferredLanguages,
|
||||
availableLocales
|
||||
)
|
||||
getPreferredLanguage(preferredLanguages, availableLocales)
|
||||
);
|
||||
}, [preferredLanguages, availableLocales, router.locale]);
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
[
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue