Skip to content

Commit 4bb1151

Browse files
authored
examples (#229)
* add examples * live demos * og's * r3f mobile * rate limits * fix lint
1 parent ad557b2 commit 4bb1151

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2154
-115
lines changed

apps/web/app/(main)/docs/layout.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DocsMobileNav } from "@/components/docs-mobile-nav";
22
import { DocsSidebar } from "@/components/docs-sidebar";
33
import { CopyPageButton } from "@/components/copy-page-button";
4+
import { TableOfContents } from "@/components/table-of-contents";
45

56
export default function DocsLayout({
67
children,
@@ -10,7 +11,7 @@ export default function DocsLayout({
1011
return (
1112
<>
1213
<DocsMobileNav />
13-
<div className="max-w-5xl mx-auto px-6 py-8 lg:py-12 flex gap-16">
14+
<div className="max-w-7xl mx-auto px-6 py-8 lg:py-12 flex gap-12">
1415
{/* Sidebar */}
1516
<aside className="w-48 shrink-0 hidden lg:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
1617
<DocsSidebar />
@@ -23,6 +24,11 @@ export default function DocsLayout({
2324
</div>
2425
<article>{children}</article>
2526
</div>
27+
28+
{/* On this page */}
29+
<aside className="w-44 shrink-0 hidden xl:block sticky top-28 h-[calc(100vh-7rem)] overflow-y-auto">
30+
<TableOfContents />
31+
</aside>
2632
</div>
2733
</>
2834
);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { Badge } from "@/components/ui/badge";
5+
import { examples, allTags, getGitHubUrl, type Example } from "@/lib/examples";
6+
import { cn } from "@/lib/utils";
7+
8+
function ExampleCard({ example }: { example: Example }) {
9+
return (
10+
<div className="group flex flex-col rounded-xl border border-border bg-card text-card-foreground overflow-hidden transition-colors hover:border-foreground/25">
11+
<div className="flex flex-1 flex-col gap-3 p-5">
12+
<h3 className="font-semibold leading-none">{example.title}</h3>
13+
14+
<p className="text-sm text-muted-foreground leading-relaxed">
15+
{example.description}
16+
</p>
17+
18+
<div className="flex flex-wrap gap-1.5">
19+
{example.tags.map((tag) => (
20+
<Badge key={tag} variant="secondary" className="text-[11px]">
21+
{tag}
22+
</Badge>
23+
))}
24+
</div>
25+
26+
<div className="mt-auto flex items-center gap-3 pt-2">
27+
{example.demoUrl && (
28+
<a
29+
href={example.demoUrl}
30+
target="_blank"
31+
rel="noopener noreferrer"
32+
className="inline-flex items-center gap-1.5 text-sm text-foreground hover:text-primary transition-colors"
33+
>
34+
<svg
35+
xmlns="http://www.w3.org/2000/svg"
36+
width="14"
37+
height="14"
38+
viewBox="0 0 24 24"
39+
fill="none"
40+
stroke="currentColor"
41+
strokeWidth="2"
42+
strokeLinecap="round"
43+
strokeLinejoin="round"
44+
>
45+
<path d="M15 3h6v6" />
46+
<path d="M10 14 21 3" />
47+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
48+
</svg>
49+
Live Demo
50+
</a>
51+
)}
52+
<a
53+
href={getGitHubUrl(example)}
54+
target="_blank"
55+
rel="noopener noreferrer"
56+
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
57+
>
58+
<svg
59+
viewBox="0 0 16 16"
60+
className="h-3.5 w-3.5"
61+
fill="currentColor"
62+
aria-hidden="true"
63+
>
64+
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
65+
</svg>
66+
Source
67+
</a>
68+
</div>
69+
</div>
70+
</div>
71+
);
72+
}
73+
74+
export default function ExamplesPage() {
75+
const [activeTag, setActiveTag] = useState<string | null>(null);
76+
77+
const filtered = activeTag
78+
? examples.filter((e) => e.tags.includes(activeTag))
79+
: examples;
80+
81+
return (
82+
<section className="mx-auto max-w-6xl px-6 py-16">
83+
<div className="mb-10">
84+
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl">
85+
Examples
86+
</h1>
87+
<p className="mt-3 text-lg text-muted-foreground">
88+
Explore json-render across frameworks, renderers, and use cases.
89+
</p>
90+
</div>
91+
92+
<div className="mb-8 flex flex-wrap gap-2">
93+
<button
94+
onClick={() => setActiveTag(null)}
95+
className={cn(
96+
"rounded-full border px-3 py-1 text-xs font-medium transition-colors",
97+
activeTag === null
98+
? "border-foreground bg-foreground text-background"
99+
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/50",
100+
)}
101+
>
102+
All
103+
</button>
104+
{allTags.map((tag) => (
105+
<button
106+
key={tag}
107+
onClick={() => setActiveTag(activeTag === tag ? null : tag)}
108+
className={cn(
109+
"rounded-full border px-3 py-1 text-xs font-medium transition-colors",
110+
activeTag === tag
111+
? "border-foreground bg-foreground text-background"
112+
: "border-border text-muted-foreground hover:text-foreground hover:border-foreground/50",
113+
)}
114+
>
115+
{tag}
116+
</button>
117+
))}
118+
</div>
119+
120+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
121+
{filtered.map((example) => (
122+
<ExampleCard key={example.slug} example={example} />
123+
))}
124+
</div>
125+
126+
{filtered.length === 0 && (
127+
<p className="py-12 text-center text-muted-foreground">
128+
No examples match the selected filter.
129+
</p>
130+
)}
131+
</section>
132+
);
133+
}

apps/web/app/api/search/route.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getSearchIndex } from "@/lib/search-index";
3+
4+
export async function GET(req: NextRequest) {
5+
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
6+
7+
if (!q) {
8+
return NextResponse.json({ results: [] });
9+
}
10+
11+
const index = await getSearchIndex();
12+
const terms = q.split(/\s+/).filter(Boolean);
13+
14+
const results = index
15+
.map((entry) => {
16+
const titleLower = entry.title.toLowerCase();
17+
const contentLower = entry.content.toLowerCase();
18+
19+
const titleMatch = terms.every((t) => titleLower.includes(t));
20+
const contentMatch = terms.every((t) => contentLower.includes(t));
21+
22+
if (!titleMatch && !contentMatch) return null;
23+
24+
let snippet = "";
25+
if (contentMatch) {
26+
const firstTermIdx = Math.min(
27+
...terms.map((t) => {
28+
const idx = contentLower.indexOf(t);
29+
return idx === -1 ? Infinity : idx;
30+
}),
31+
);
32+
if (firstTermIdx !== Infinity) {
33+
const start = Math.max(0, firstTermIdx - 40);
34+
const end = Math.min(entry.content.length, firstTermIdx + 120);
35+
snippet =
36+
(start > 0 ? "..." : "") +
37+
entry.content.slice(start, end).replace(/\n/g, " ") +
38+
(end < entry.content.length ? "..." : "");
39+
}
40+
}
41+
42+
return {
43+
title: entry.title,
44+
href: entry.href,
45+
section: entry.section,
46+
snippet,
47+
score: titleMatch ? 2 : 1,
48+
};
49+
})
50+
.filter(
51+
(
52+
r,
53+
): r is {
54+
title: string;
55+
href: string;
56+
section: string;
57+
snippet: string;
58+
score: number;
59+
} => r !== null,
60+
)
61+
.sort((a, b) => b.score - a.score)
62+
.slice(0, 20)
63+
.map(({ title, href, section, snippet }) => ({
64+
title,
65+
href,
66+
section,
67+
snippet,
68+
}));
69+
70+
return NextResponse.json(
71+
{ results },
72+
{ headers: { "Cache-Control": "public, max-age=60" } },
73+
);
74+
}

apps/web/components/docs-chat.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,10 +261,10 @@ export function DocsChat({
261261
}
262262
}, [messages, isLoading]);
263263

264-
// Cmd+K to open sidebar and focus prompt, Escape to close
264+
// Cmd+I to open sidebar and focus prompt, Escape to close
265265
useEffect(() => {
266266
const handleKeyDown = (e: KeyboardEvent) => {
267-
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
267+
if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
268268
e.preventDefault();
269269
setOpen((prev) => {
270270
if (!prev) {
@@ -497,12 +497,12 @@ export function DocsChat({
497497
{!open && (
498498
<button
499499
onClick={() => setOpen(true)}
500-
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border bg-background text-primary shadow-lg hover:bg-primary hover:text-primary-foreground transition-colors text-sm font-medium"
500+
className="fixed z-50 bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:translate-x-0 sm:right-4 flex items-center gap-2 px-4 py-2 rounded-lg border border-primary bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors text-sm font-medium"
501501
aria-label="Ask AI"
502502
>
503503
Ask AI
504504
<kbd className="hidden sm:inline-flex items-center gap-0.5 text-xs opacity-60 font-mono">
505-
<span>&#8984;</span>K
505+
<span>&#8984;</span>I
506506
</kbd>
507507
</button>
508508
)}

0 commit comments

Comments
 (0)