diff --git a/src/components/AnchorShare.tsx b/src/components/AnchorShare.tsx index 0676cf4..df24492 100644 --- a/src/components/AnchorShare.tsx +++ b/src/components/AnchorShare.tsx @@ -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 => ( ( > { navigator.clipboard.writeText( `${ diff --git a/src/components/Chronicles/ChroniclesList.tsx b/src/components/Chronicles/ChroniclesList.tsx index 239c484..fd9df24 100644 --- a/src/components/Chronicles/ChroniclesList.tsx +++ b/src/components/Chronicles/ChroniclesList.tsx @@ -64,7 +64,6 @@ const ChroniclesList = ({
{chronicle.attributes.translations.length === 0 && chronicle.attributes.contents.data.length === 1 diff --git a/src/components/Markdown/Markdawn.tsx b/src/components/Markdown/Markdawn.tsx index 1473993..534e681 100644 --- a/src/components/Markdown/Markdawn.tsx +++ b/src/components/Markdown/Markdawn.tsx @@ -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; }) => ( -

- {compProps.children} - -

- ), - }, - - h2: { - component: (compProps: { - id: string; - style: React.CSSProperties; - children: React.ReactNode; - }) => ( -

- {compProps.children} - -

- ), - }, - - h3: { - component: (compProps: { - id: string; - style: React.CSSProperties; - children: React.ReactNode; - }) => ( -

- {compProps.children} - -

- ), - }, - - h4: { - component: (compProps: { - id: string; - style: React.CSSProperties; - children: React.ReactNode; - }) => ( -

- {compProps.children} - -

- ), - }, - - h5: { - component: (compProps: { - id: string; - style: React.CSSProperties; - children: React.ReactNode; - }) => ( -
- {compProps.children} - -
- ), - }, - - h6: { - component: (compProps: { - id: string; - style: React.CSSProperties; - children: React.ReactNode; - }) => ( -
- {compProps.children} - -
+
), }, SceneBreak: { component: (compProps: { id: string }) => ( -
- * * * -
+
), }, @@ -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 ( <> -

{langui.table_of_contents}

- + {toc.children.length > 0 && ( + <> + {horizontalLine && } +

{langui.table_of_contents}

+ + + )} ); }; @@ -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( + () => ( + <> +
+ {title === "* * *" ? ( +
+ + + +
+ ) : ( +
{title}
+ )} + +
+ + ), + [langui, slug, title] + ); + + const className = "group"; + + switch (level) { + case 1: + return ( +

+ {innerComponent} +

+ ); + case 2: + return ( +

+ {innerComponent} +

+ ); + case 3: + return ( +

+ {innerComponent} +

+ ); + case 4: + return ( +

+ {innerComponent} +

+ ); + case 5: + return ( +
+ {innerComponent} +
+ ); + default: + return ( +
+ {innerComponent} +
+ ); + } +}; + 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 (
    {tocchildren.map((child, childIndex) => ( -
  1. +
  2. {`${parentNumbering}${ childIndex + 1 }.`}{" "} @@ -377,6 +415,9 @@ const TocLevel = ({ ))} @@ -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 ``; } - 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 `${lineText}`; + return `
    ${lineText}
    `; }; // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ @@ -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("

    { h4 = -1; h5 = -1; scenebreak = 0; - } else if (h2 >= 0 && line.startsWith("

    { h4 = -1; h5 = -1; scenebreak = 0; - } else if (h3 >= 0 && line.startsWith("

    { h4++; h5 = -1; scenebreak = 0; - } else if (h4 >= 0 && line.startsWith("

    { }); h5++; scenebreak = 0; - } else if (h5 >= 0 && line.startsWith("
    ; } @@ -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 ( diff --git a/src/components/RecorderChip.tsx b/src/components/RecorderChip.tsx index 4846c46..6d34635 100644 --- a/src/components/RecorderChip.tsx +++ b/src/components/RecorderChip.tsx @@ -28,7 +28,7 @@ export const RecorderChip = ({ recorder, langui }: Props): JSX.Element => (
    {recorder.avatar?.data?.attributes && ( diff --git a/src/hooks/useIntersectionList.ts b/src/hooks/useIntersectionList.ts new file mode 100644 index 0000000..7ca55e3 --- /dev/null +++ b/src/hooks/useIntersectionList.ts @@ -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; +}; diff --git a/src/hooks/useScrollTopOnChange.ts b/src/hooks/useScrollTopOnChange.ts index 322801f..1ad675b 100644 --- a/src/hooks/useScrollTopOnChange.ts +++ b/src/hooks/useScrollTopOnChange.ts @@ -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]); +}; diff --git a/src/hooks/useSmartLanguage.ts b/src/hooks/useSmartLanguage.ts index 42242f0..f367dfc 100644 --- a/src/hooks/useSmartLanguage.ts +++ b/src/hooks/useSmartLanguage.ts @@ -41,10 +41,7 @@ export const useSmartLanguage = ({ useEffect(() => { setSelectedTranslationIndex( - getPreferredLanguage( - preferredLanguages, - availableLocales - ) + getPreferredLanguage(preferredLanguages, availableLocales) ); }, [preferredLanguages, availableLocales, router.locale]); diff --git a/src/pages/api/revalidate.ts b/src/pages/api/revalidate.ts index 0398abb..ba9d850 100644 --- a/src/pages/api/revalidate.ts +++ b/src/pages/api/revalidate.ts @@ -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}`); diff --git a/src/pages/contents/[slug].tsx b/src/pages/contents/[slug].tsx index ff02bf1..486ec8e 100644 --- a/src/pages/contents/[slug].tsx +++ b/src/pages/contents/[slug].tsx @@ -123,128 +123,145 @@ const Content = ({ {selectedTranslation?.text_set?.source_language?.data?.attributes ?.code !== undefined && ( -
    -

    + <> + +
    +

    + {selectedTranslation.text_set.source_language.data.attributes + .code === selectedTranslation.language?.data?.attributes?.code + ? langui.transcript_notice + : langui.translation_notice} +

    + {selectedTranslation.text_set.source_language.data.attributes - .code === selectedTranslation.language?.data?.attributes?.code - ? langui.transcript_notice - : langui.translation_notice} -

    - - {selectedTranslation.text_set.source_language.data.attributes - .code !== - selectedTranslation.language?.data?.attributes?.code && ( -
    -

    - {langui.source_language}: -

    - -
    - )} - -
    -

    {langui.status}:

    - - - - -
    - - {selectedTranslation.text_set.transcribers && - selectedTranslation.text_set.transcribers.data.length > 0 && ( -
    + .code !== + selectedTranslation.language?.data?.attributes?.code && ( +

    - {langui.transcribers}: + {langui.source_language}:

    -
    - {filterHasAttributes( - selectedTranslation.text_set.transcribers.data, - ["attributes", "id"] as const - ).map((recorder) => ( - - - - ))} -
    -
    - )} - - {selectedTranslation.text_set.translators && - selectedTranslation.text_set.translators.data.length > 0 && ( -
    -

    - {langui.translators}: -

    -
    - {filterHasAttributes( - selectedTranslation.text_set.translators.data, - ["attributes", "id"] as const - ).map((recorder) => ( - - - - ))} -
    -
    - )} - - {selectedTranslation.text_set.proofreaders && - selectedTranslation.text_set.proofreaders.data.length > 0 && ( -
    -

    - {langui.proofreaders}: -

    -
    - {filterHasAttributes( - selectedTranslation.text_set.proofreaders.data, - ["attributes", "id"] as const - ).map((recorder) => ( - - - - ))} -
    -
    - )} - - {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( -
    -

    {langui.notes}:

    -
    -
    + )} + +
    +

    {langui.status}:

    + + + +
    - )} -
    + + {selectedTranslation.text_set.transcribers && + selectedTranslation.text_set.transcribers.data.length > 0 && ( +
    +

    + {langui.transcribers}: +

    +
    + {filterHasAttributes( + selectedTranslation.text_set.transcribers.data, + ["attributes", "id"] as const + ).map((recorder) => ( + + + + ))} +
    +
    + )} + + {selectedTranslation.text_set.translators && + selectedTranslation.text_set.translators.data.length > 0 && ( +
    +

    + {langui.translators}: +

    +
    + {filterHasAttributes( + selectedTranslation.text_set.translators.data, + ["attributes", "id"] as const + ).map((recorder) => ( + + + + ))} +
    +
    + )} + + {selectedTranslation.text_set.proofreaders && + selectedTranslation.text_set.proofreaders.data.length > 0 && ( +
    +

    + {langui.proofreaders}: +

    +
    + {filterHasAttributes( + selectedTranslation.text_set.proofreaders.data, + ["attributes", "id"] as const + ).map((recorder) => ( + + + + ))} +
    +
    + )} + + {isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && ( +
    +

    {langui.notes}:

    +
    + +
    +
    + )} +
    + + )} + + {selectedTranslation?.text_set?.text && ( + <> + + )} {content.ranged_contents?.data && @@ -315,21 +332,6 @@ const Content = ({
    )} - - {selectedTranslation?.text_set?.text && ( - <> - - - - )} ), [ diff --git a/src/pages/library/[slug]/index.tsx b/src/pages/library/[slug]/index.tsx index 294d675..151a00d 100644 --- a/src/pages/library/[slug]/index.tsx +++ b/src/pages/library/[slug]/index.tsx @@ -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 = ({ />
    - + {item.gallery && item.gallery.data.length > 0 && ( - + )} - + {item.subitems && item.subitems.data.length > 0 && ( )} {item.contents && item.contents.data.length > 0 && ( - + )}
    ), - [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 = ({ )}
    - +
    {item.subitem_of?.data[0]?.attributes && (
    @@ -246,7 +290,10 @@ const LibrarySlug = ({ {item.gallery && item.gallery.data.length > 0 && ( -