Skip to content

Commit 1a80a97

Browse files
authored
Merge pull request #61 from hasparus/learn-faq
Aggregated FAQ
2 parents f19a34b + 7b45ba1 commit 1a80a97

36 files changed

+691
-51
lines changed

src/_design-system/mdx-components/get-mdx-headings.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const createHeading = (
3030
id,
3131
className,
3232
...props
33-
}: React.ComponentPropsWithoutRef<"h2">): React.ReactElement {
33+
}: React.ComponentPropsWithoutRef<"h2"> & {
34+
size?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
35+
}): React.ReactElement {
3436
// Nextra tracks anchors in context
3537
const setActiveAnchor = useSetActiveAnchor()
3638
const slugs = useSlugs()
@@ -61,7 +63,7 @@ const createHeading = (
6163
className === "sr-only"
6264
? // can be added by footnotes
6365
"sr-only"
64-
: clsx(headingClasses[Tag], "text-neu-900", className)
66+
: clsx(headingClasses[props.size || Tag], "text-neu-900", className)
6567
}
6668
{...props}
6769
>
Lines changed: 17 additions & 0 deletions
Loading
Lines changed: 17 additions & 0 deletions
Loading
Lines changed: 21 additions & 0 deletions
Loading
Lines changed: 17 additions & 0 deletions
Loading
Lines changed: 17 additions & 0 deletions
Loading
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"use client"
2+
3+
import { getMdxHeadings } from "@/_design-system/mdx-components/get-mdx-headings"
4+
import { type ReactNode, useRef, useLayoutEffect, useState } from "react"
5+
import ArrowDown from "@/app/conf/_design-system/pixelarticons/arrow-down.svg?svgr"
6+
import BestPracticesIcon from "./decorations/best-practices.svg?svgr"
7+
import FrontendIcon from "./decorations/frontend.svg?svgr"
8+
import GeneralIcon from "./decorations/general.svg?svgr"
9+
import GettingStartedIcon from "./decorations/getting-started.svg?svgr"
10+
import SpecificationIcon from "./decorations/specification.svg?svgr"
11+
12+
function slugify(text: string): string {
13+
return String(text)
14+
.toLowerCase()
15+
.replace(/[^a-z0-9]+/g, "-")
16+
.replace(/(^-|-$)/g, "")
17+
}
18+
19+
const mdxHeadings = getMdxHeadings()
20+
21+
const iconMap: Record<string, typeof GettingStartedIcon> = {
22+
"getting-started": GettingStartedIcon,
23+
"best-practices": BestPracticesIcon,
24+
frontend: FrontendIcon,
25+
general: GeneralIcon,
26+
specification: SpecificationIcon,
27+
}
28+
29+
function FaqH1({ id, children }: { id?: string; children?: ReactNode }) {
30+
const slug = id ?? slugify(String(children))
31+
const Icon = iconMap[slug] ?? GeneralIcon
32+
33+
return (
34+
<div
35+
data-heading
36+
className="mb-4 mt-8 flex items-center border border-neu-400 first:mt-0 dark:border-neu-100"
37+
>
38+
<div className="flex size-[90px] shrink-0 items-center justify-center border-r border-inherit bg-neu-100 p-4 dark:bg-neu-50/50">
39+
<Icon className="size-full text-neu-800" />
40+
</div>
41+
<mdxHeadings.h2
42+
id={slug}
43+
size="h1"
44+
className="!mt-0 flex-1 px-4 text-neu-900"
45+
>
46+
{children}
47+
</mdxHeadings.h2>
48+
</div>
49+
)
50+
}
51+
52+
function FaqH2({ id, children }: { id?: string; children?: ReactNode }) {
53+
const slug = id ?? slugify(String(children))
54+
return (
55+
<details className="group mt-4 border border-neu-400 bg-neu-0 *:px-3 dark:border-neu-100 lg:mt-6 [&:first-of-type]:border-t [&>:last-child]:mb-3 [&>p:first-of-type]:!mt-3">
56+
<summary className="gql-focus-visible flex cursor-pointer list-none items-center justify-between gap-4 p-3 hover:bg-neu-100 group-open:border-b group-open:border-neu-400 dark:hover:bg-neu-50/50 dark:group-open:border-neu-100 [&::-webkit-details-marker]:hidden">
57+
<h3 id={slug} className="typography-body-lg text-neu-900">
58+
{children}
59+
</h3>
60+
<ArrowDown className="size-10 shrink-0 text-neu-800 group-open:rotate-180" />
61+
</summary>
62+
</details>
63+
)
64+
}
65+
66+
export function FaqAggregator({ children }: { children: ReactNode }) {
67+
const containerRef = useRef<HTMLDivElement>(null)
68+
const [, forceUpdate] = useState(0)
69+
70+
useLayoutEffect(() => {
71+
const container = containerRef.current
72+
if (!container) return
73+
74+
const details = container.querySelectorAll("details")
75+
details.forEach(detail => {
76+
const answerContent: Node[] = []
77+
let sibling = detail.nextSibling
78+
79+
while (sibling) {
80+
const next = sibling.nextSibling
81+
if (sibling instanceof Element) {
82+
if (
83+
sibling.tagName === "DETAILS" ||
84+
(sibling as HTMLElement).dataset?.heading
85+
)
86+
break
87+
answerContent.push(sibling)
88+
} else if (
89+
sibling.nodeType === Node.TEXT_NODE &&
90+
sibling.textContent?.trim()
91+
) {
92+
answerContent.push(sibling)
93+
}
94+
sibling = next
95+
}
96+
97+
if (answerContent.length > 0) {
98+
answerContent.forEach(node => detail.appendChild(node))
99+
}
100+
})
101+
102+
forceUpdate(n => n + 1)
103+
}, [])
104+
105+
return <div ref={containerRef}>{children}</div>
106+
}
107+
108+
export const faqMdxComponents = {
109+
h1: FaqH1,
110+
h2: FaqH2,
111+
}
Lines changed: 19 additions & 1 deletion
Loading
Lines changed: 16 additions & 1 deletion
Loading
Lines changed: 19 additions & 1 deletion
Loading

0 commit comments

Comments
 (0)