Skip to content

Commit 16c1457

Browse files
committed
wip
1 parent abb04fe commit 16c1457

File tree

10 files changed

+837
-88
lines changed

10 files changed

+837
-88
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client"
2+
3+
import { Button } from "@/app/conf/_design-system/button"
4+
import { Eyebrow } from "@/_design-system/eyebrow"
5+
import { BlogCard } from "@/components/blog-page/blog-card"
6+
7+
export interface BlogPost {
8+
href: string
9+
title: string
10+
author: string
11+
date?: Date
12+
tags: string[]
13+
}
14+
15+
export interface BlogPostsSectionProps {
16+
title: string
17+
description: string
18+
posts: BlogPost[]
19+
readAllHref?: string
20+
readAllLabel?: string
21+
}
22+
23+
export function BlogPostsSection({
24+
title,
25+
description,
26+
posts,
27+
readAllHref = "/blog",
28+
readAllLabel = "Read all GraphQL stories",
29+
}: BlogPostsSectionProps) {
30+
return (
31+
<section className="flex flex-col gap-10 lg:gap-16">
32+
<header className="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
33+
<div className="flex flex-col gap-3">
34+
<Eyebrow>Blog posts</Eyebrow>
35+
<h2 className="typography-h2 max-w-[700px] text-pretty">{title}</h2>
36+
<p className="typography-body-md max-w-[577px] text-neu-800">
37+
{description}
38+
</p>
39+
</div>
40+
<Button href={readAllHref} variant="secondary" size="md">
41+
{readAllLabel}
42+
</Button>
43+
</header>
44+
45+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
46+
{posts.map(post => (
47+
<BlogCard
48+
key={post.href}
49+
route={post.href}
50+
frontMatter={{
51+
title: post.title,
52+
byline: post.author,
53+
date: post.date ?? new Date(),
54+
tags: post.tags,
55+
}}
56+
/>
57+
))}
58+
</div>
59+
</section>
60+
)
61+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import path from "node:path"
2+
import { glob } from "node:fs/promises"
3+
import { readFile } from "node:fs/promises"
4+
import matter from "gray-matter"
5+
6+
import { clsx } from "clsx"
7+
import { Button } from "@/app/conf/_design-system/button"
8+
import { Eyebrow } from "@/_design-system/eyebrow"
9+
import slugMap from "@/code/slug-map.json"
10+
import { type Topic } from "@/resources/types"
11+
12+
interface LibraryEntry {
13+
name: string
14+
href?: string
15+
group: string
16+
tags: string[]
17+
}
18+
19+
const librariesPromise = loadLibraries()
20+
21+
async function loadLibraries(): Promise<LibraryEntry[]> {
22+
const entries: LibraryEntry[] = []
23+
24+
for await (const file of glob("src/code/**/*.md")) {
25+
const relative = path.relative("src/code", file)
26+
const segments = relative.split(path.sep)
27+
const top = segments[0]
28+
const group =
29+
top === "language-support" ? (segments[1] ?? "language-support") : top
30+
if (!group) continue
31+
32+
const raw = await readFile(file, "utf8")
33+
const { data } = matter(raw)
34+
const tags: string[] = Array.isArray(data.tags) ? data.tags : []
35+
if (!tags.includes("tools-and-libraries")) continue
36+
37+
const name: string | undefined = data.name
38+
if (!name) continue
39+
40+
const href: string | undefined =
41+
data.url ??
42+
(data.github ? `https://github.com/${data.github}` : undefined) ??
43+
(data.npm ? `https://npmjs.com/package/${data.npm}` : undefined)
44+
45+
entries.push({ name, href, group, tags })
46+
}
47+
48+
return entries
49+
}
50+
51+
function displayName(id: string) {
52+
const key = id as keyof typeof slugMap
53+
return slugMap[key] ?? id
54+
}
55+
56+
export async function CategoryToolsLibrariesSection({
57+
category,
58+
}: {
59+
category: Topic
60+
}) {
61+
const libraries = await librariesPromise
62+
const filtered = libraries.filter(item => item.tags.includes(category))
63+
64+
const grouped = Array.from(
65+
filtered.reduce<Map<string, LibraryEntry[]>>((acc, item) => {
66+
const list = acc.get(item.group) ?? []
67+
list.push(item)
68+
acc.set(item.group, list)
69+
return acc
70+
}, new Map()),
71+
)
72+
.map(([group, items]) => ({
73+
id: group,
74+
name: displayName(group),
75+
items: items
76+
.sort((a, b) =>
77+
a.name.localeCompare(b.name, "en", { sensitivity: "base" }),
78+
)
79+
.slice(0, 20),
80+
}))
81+
.sort((a, b) => a.name.localeCompare(b.name, "en", { sensitivity: "base" }))
82+
83+
if (grouped.length === 0) {
84+
return null
85+
}
86+
87+
const desktopLayoutClass =
88+
grouped.length > 2 ? "lg:grid lg:grid-cols-1 lg:gap-6" : "lg:grid lg:grid-cols-2 lg:gap-6"
89+
90+
return (
91+
<section
92+
id="tools-and-libraries"
93+
className="flex flex-col gap-8 border border-sec-base bg-sec-lighter p-6 dark:border-sec-darker dark:bg-sec-darker/15 lg:gap-10 lg:p-10"
94+
>
95+
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
96+
<div className="flex flex-col gap-3">
97+
<Eyebrow className="!text-sec-darker dark:!text-sec-light">
98+
key tools & libraries
99+
</Eyebrow>
100+
<h2 className="typography-h3 text-pretty">
101+
Build GraphQL with tools and libraries
102+
</h2>
103+
<p className="typography-body-md text-neu-800">
104+
Explore language and platform tooling to ship production-ready
105+
graphs.
106+
</p>
107+
</div>
108+
<Button href="/code" variant="tertiary" className="w-fit">
109+
See all Tools & Libraries
110+
</Button>
111+
</div>
112+
113+
<div
114+
className={clsx(
115+
"flex gap-4 overflow-x-auto pb-2 lg:overflow-visible",
116+
desktopLayoutClass,
117+
)}
118+
>
119+
{grouped.map(group => (
120+
<div
121+
key={group.id}
122+
className="min-w-[280px] shrink-0 border border-neu-200 bg-neu-0 shadow-[0_1px_0_#E5E7EB] dark:border-neu-100 dark:bg-neu-0/60 lg:min-w-0"
123+
>
124+
<div className="flex items-center gap-3 border-b border-neu-200 px-4 py-3 text-neu-900 dark:border-neu-100">
125+
<span className="font-mono text-sm uppercase text-neu-700">
126+
{group.name}
127+
</span>
128+
</div>
129+
<ul className="divide-y divide-neu-200 dark:divide-neu-100">
130+
{group.items.map(item => (
131+
<li key={`${group.id}-${item.name}`}>
132+
{item.href ? (
133+
<a
134+
href={item.href}
135+
className="flex items-center justify-between px-4 py-3 text-neu-900 transition-colors hover:bg-neu-50 dark:hover:bg-neu-50/50"
136+
>
137+
<span>{item.name}</span>
138+
<span aria-hidden className="text-neu-500">
139+
140+
</span>
141+
</a>
142+
) : (
143+
<span className="flex items-center justify-between px-4 py-3 text-neu-900">
144+
<span>{item.name}</span>
145+
</span>
146+
)}
147+
</li>
148+
))}
149+
</ul>
150+
</div>
151+
))}
152+
</div>
153+
</section>
154+
)
155+
}

0 commit comments

Comments
 (0)