Skip to content

Commit 2a4ed53

Browse files
authored
feat: add glossary pages (#45)
1 parent 6d5e8d9 commit 2a4ed53

File tree

13 files changed

+404
-0
lines changed

13 files changed

+404
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use client"
2+
3+
import { Letter } from './letter'
4+
5+
export function AlphabetNav() {
6+
return (
7+
<div className="flex flex-wrap gap-2 mb-12">
8+
{Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ").map((letter) => (
9+
<Letter key={letter} letter={letter} />
10+
))}
11+
</div>
12+
)
13+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getGlossaryTerms } from '@app/_utils/get-glossary-terms'
2+
import { AlphabetNav } from './alphabet-nav'
3+
import { TermsList } from './terms-list'
4+
5+
export async function GlossaryContent() {
6+
const terms = await getGlossaryTerms()
7+
8+
return (
9+
<>
10+
<h1 className="text-4xl font-bold mb-8">UX Patterns Glossary</h1>
11+
<p className="text-lg mb-12">
12+
A comprehensive glossary of UX patterns and web development terminology. This resource is designed to help developers and designers understand the key concepts, patterns, and principles discussed throughout our documentation.
13+
</p>
14+
15+
<AlphabetNav />
16+
<TermsList terms={terms} />
17+
</>
18+
)
19+
}

app/_components/glossary/letter.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client"
2+
3+
import Link from 'next/link'
4+
import { usePathname } from 'next/navigation'
5+
6+
interface LetterProps {
7+
letter: string
8+
}
9+
10+
export function Letter({ letter }: LetterProps) {
11+
const pathname = usePathname()
12+
const isActive = pathname.includes(`#${letter.toLowerCase()}`)
13+
14+
return (
15+
<Link
16+
href={`#${letter.toLowerCase()}`}
17+
className={`
18+
px-3 py-2 rounded-md text-sm font-medium
19+
${isActive
20+
? 'bg-primary text-white'
21+
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
22+
}
23+
`}
24+
scroll={false}
25+
>
26+
{letter}
27+
</Link>
28+
)
29+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { JsonLd } from '@app/_components/json-ld'
2+
3+
interface GlossaryStructuredDataProps {
4+
term: string
5+
definition: string
6+
category: string[]
7+
}
8+
9+
export function GlossaryStructuredData({
10+
term,
11+
definition,
12+
category
13+
}: GlossaryStructuredDataProps) {
14+
const structuredData = {
15+
'@context': 'https://schema.org',
16+
'@type': 'DefinedTerm',
17+
name: term,
18+
description: definition,
19+
inDefinedTermSet: {
20+
'@type': 'DefinedTermSet',
21+
name: 'UX Patterns Glossary',
22+
url: 'https://uxpatterns.dev/glossary'
23+
},
24+
termCode: category.join(',')
25+
}
26+
27+
return <JsonLd data={structuredData} />
28+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client"
2+
3+
import Link from 'next/link'
4+
5+
interface TermCardProps {
6+
title: string
7+
description: string
8+
category: string[]
9+
slug: string
10+
}
11+
12+
export function TermCard({ title, description, category, slug }: TermCardProps) {
13+
return (
14+
<Link
15+
href={`./glossary/${slug}`}
16+
className="block p-6 rounded-lg border border-gray-200 hover:border-primary transition-colors duration-200 dark:border-gray-700"
17+
>
18+
<div className="flex flex-col gap-2">
19+
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
20+
{title}
21+
</h3>
22+
<p className="text-sm text-gray-600 dark:text-gray-300">
23+
{description}
24+
</p>
25+
<div className="flex flex-wrap gap-2 mt-2">
26+
{category.map((cat) => (
27+
<span
28+
key={cat}
29+
className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300"
30+
>
31+
{cat}
32+
</span>
33+
))}
34+
</div>
35+
</div>
36+
</Link>
37+
)
38+
}
39+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
import { GlossaryStructuredData } from './structured-data'
3+
4+
interface TermPageProps {
5+
frontMatter: {
6+
title: string
7+
description: string
8+
category: string[]
9+
related_patterns?: string[]
10+
synonyms?: string[]
11+
}
12+
children: React.ReactNode
13+
}
14+
15+
export function TermPage({ frontMatter, children }: TermPageProps) {
16+
return (
17+
<div className="max-w-4xl mx-auto">
18+
<GlossaryStructuredData
19+
term={frontMatter.title}
20+
definition={frontMatter.description}
21+
category={frontMatter.category}
22+
/>
23+
{children}
24+
</div>
25+
)
26+
}
27+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client"
2+
3+
import { TermCard } from './term-card'
4+
5+
interface Term {
6+
title: string
7+
description: string
8+
category: string[]
9+
slug: string
10+
}
11+
12+
interface TermsListProps {
13+
terms: Term[]
14+
}
15+
16+
export function TermsList({ terms }: TermsListProps) {
17+
// Group terms by first letter
18+
const groupedTerms = terms.reduce<Record<string, Term[]>>((acc, term) => {
19+
// Ensure title exists and has at least one character
20+
const title = term.title || ''
21+
if (title.length === 0) return acc
22+
23+
// Get the first character and ensure it's a letter
24+
const firstChar = title[0]
25+
if (!firstChar) return acc
26+
27+
const firstLetter = firstChar.toUpperCase()
28+
if (!acc[firstLetter]) {
29+
acc[firstLetter] = []
30+
}
31+
acc[firstLetter].push(term)
32+
return acc
33+
}, {})
34+
35+
// Sort letters alphabetically
36+
const sortedLetters = Object.keys(groupedTerms).sort()
37+
38+
if (sortedLetters.length === 0) {
39+
return <p>No glossary terms found.</p>
40+
}
41+
42+
return (
43+
<div className="space-y-12">
44+
{sortedLetters.map((letter) => {
45+
const letterTerms = groupedTerms[letter]
46+
if (!letterTerms?.length) return null
47+
48+
return (
49+
<section key={letter} id={letter.toLowerCase()}>
50+
<h2 className="text-2xl font-bold mb-6 text-gray-900 dark:text-white scroll-mt-20">
51+
{letter}
52+
</h2>
53+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
54+
{letterTerms
55+
.sort((a, b) => a.title.localeCompare(b.title))
56+
.map((term) => (
57+
<TermCard
58+
key={term.slug}
59+
title={term.title}
60+
description={term.description}
61+
category={term.category}
62+
slug={term.slug}
63+
/>
64+
))}
65+
</div>
66+
</section>
67+
)
68+
})}
69+
</div>
70+
)
71+
}
72+

app/_utils/get-glossary-terms.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { readdir, readFile } from 'fs/promises'
2+
import matter from 'gray-matter'
3+
import { join } from 'path'
4+
5+
interface GlossaryTerm {
6+
title: string
7+
description: string
8+
category: string[]
9+
slug: string
10+
}
11+
12+
export async function getGlossaryTerms(): Promise<GlossaryTerm[]> {
13+
const glossaryPath = join(process.cwd(), 'content/en/glossary')
14+
const files = await readdir(glossaryPath)
15+
16+
const terms = await Promise.all(
17+
files
18+
.filter((file) => file.endsWith('.mdx') && file !== 'index.mdx')
19+
.map(async (file) => {
20+
const filePath = join(glossaryPath, file)
21+
const content = await readFile(filePath, 'utf-8')
22+
const { data } = matter(content)
23+
24+
return {
25+
title: data.title,
26+
description: data.description,
27+
category: data.category,
28+
slug: file.replace('.mdx', '')
29+
}
30+
})
31+
)
32+
33+
return terms
34+
}

content/en/_meta.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ export default {
2020
layout: 'full'
2121
}
2222
},
23+
glossary: {
24+
type: 'page',
25+
title: 'Glossary',
26+
theme: {
27+
sidebar: true,
28+
typesetting: 'article',
29+
layout: 'full'
30+
}
31+
},
2332
blog: {
2433
type: 'page',
2534
title: 'Blog',

content/en/glossary/_meta.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
'index': '',
3+
'pagination': '',
4+
}

0 commit comments

Comments
 (0)