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} | ||||
|             /> | ||||
|  | ||||
							
								
								
									
										51
									
								
								src/hooks/useIntersectionList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/hooks/useIntersectionList.ts
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint