Skip to content

Commit d80db64

Browse files
authored
Merge pull request #2 from smammar100/claude/dev-server-setup-ab354e
Sanity-back the icon gallery; add SEO agent, prompts, and studio
2 parents c2ff6cc + eb1d3e6 commit d80db64

55 files changed

Lines changed: 14438 additions & 321 deletions

Some content is hidden

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

app/api/prompt/[slug]/route.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { NextResponse } from "next/server";
2+
import { visibleIconMeta } from "@/registry/icon-meta.gen";
3+
4+
/**
5+
* The icon's AI prompt, read from Sanity.
6+
*
7+
* This is the site's ONLY read from Sanity, and it is deliberately confined to
8+
* one field on one route. `aiPrompt` is the one thing Sanity is authoritative
9+
* for — it exists nowhere in the repo — whereas the grid, ICON_COUNT and the
10+
* copy button stay build-time or static (/r/*.json) so a Sanity outage can never
11+
* take the site down. The blast radius of Sanity being unreachable is this route
12+
* returning 502 and one button showing an error toast.
13+
*
14+
* Server-side on purpose: the projectId/dataset never have to be shipped to the
15+
* browser, and the ISR cache means Sanity sees roughly one request per icon per
16+
* revalidate window rather than one per visitor. No token is needed — the
17+
* dataset is public and this reads published content only.
18+
*/
19+
20+
const PROJECT_ID = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
21+
const DATASET = process.env.NEXT_PUBLIC_SANITY_DATASET;
22+
const API_VERSION = process.env.NEXT_PUBLIC_SANITY_API_VERSION ?? "2025-02-19";
23+
24+
// Studio edits appear within this window. Prompts change rarely, so 5 minutes
25+
// trades negligible staleness for a near-zero request rate against Sanity.
26+
const REVALIDATE_SECONDS = 300;
27+
28+
export async function GET(_req: Request, ctx: { params: Promise<{ slug: string }> }) {
29+
const { slug } = await ctx.params;
30+
31+
// Validate against the registry rather than passing the path straight into a
32+
// query: the slug is user-controlled, and this keeps an arbitrary string from
33+
// ever reaching GROQ.
34+
if (!visibleIconMeta.some((e) => e.slug === slug)) {
35+
return NextResponse.json({ error: `unknown icon "${slug}"` }, { status: 404 });
36+
}
37+
38+
if (!PROJECT_ID || !DATASET) {
39+
return NextResponse.json(
40+
{ error: "Sanity is not configured (NEXT_PUBLIC_SANITY_PROJECT_ID / _DATASET)" },
41+
{ status: 501 },
42+
);
43+
}
44+
45+
const query = `*[_type == "icon" && slug == $slug][0].aiPrompt`;
46+
const url =
47+
`https://${PROJECT_ID}.apicdn.sanity.io/v${API_VERSION}/data/query/${DATASET}` +
48+
`?query=${encodeURIComponent(query)}` +
49+
`&$slug=${encodeURIComponent(JSON.stringify(slug))}`;
50+
51+
try {
52+
const res = await fetch(url, { next: { revalidate: REVALIDATE_SECONDS } });
53+
if (!res.ok) {
54+
return NextResponse.json({ error: `Sanity responded ${res.status}` }, { status: 502 });
55+
}
56+
const { result } = (await res.json()) as { result?: string | null };
57+
if (!result) {
58+
return NextResponse.json({ error: `no prompt written for "${slug}" yet` }, { status: 404 });
59+
}
60+
return NextResponse.json({ slug, prompt: result });
61+
} catch {
62+
return NextResponse.json({ error: "could not reach Sanity" }, { status: 502 });
63+
}
64+
}

app/compare/iconimate-vs-lucide-animated/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export default function ComparePage() {
4949
</tr>
5050
<tr>
5151
<td>Distribution</td>
52-
<td>shadcn registry, Open in v0</td>
52+
<td>shadcn registry, per-icon AI prompt</td>
5353
<td>Varies (npm package or copy-paste)</td>
5454
</tr>
5555
<tr>

app/docs/page.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { DocShell } from "@/components/seo/doc-shell";
33
import { ICON_COUNT } from "@/lib/seo";
44

55
export const metadata: Metadata = {
6-
title: "Docs",
6+
// The page is a substantial install guide, so the title names what it teaches
7+
// rather than labelling the section. The layout template appends "· Iconimate".
8+
title: "Install Animated React Icons",
79
description:
8-
"Install and use Iconimate animated React icons: the shadcn registry flow, Open in v0, " +
9-
"controlling animation with a ref, TypeScript usage, and reduced-motion.",
10+
"Install and use Iconimate animated React icons: the shadcn registry flow, the per-icon AI " +
11+
"prompt, controlling animation with a ref, TypeScript usage, and reduced-motion.",
1012
alternates: { canonical: "/docs" },
1113
};
1214

@@ -76,11 +78,14 @@ export function Example() {
7678
}`}</code>
7779
</pre>
7880

79-
<h2>Open in v0</h2>
81+
<h2>Copy AI prompt</h2>
8082
<p>
81-
Every icon in the gallery has an <strong>Open in v0</strong> action that hands the registry
82-
item to <a href="https://v0.dev" target="_blank" rel="noreferrer">v0</a>, so you can drop it
83-
into a v0 chat and keep building.
83+
Every icon in the gallery has a <strong>Copy AI prompt</strong> action. It gives you a
84+
self-contained brief for that icon — the glyph&apos;s subpaths, the motion it plays, the
85+
alternatives that were explored before it shipped, and the rules every Iconimate icon follows
86+
(imperative handle, <code>normal</code>/<code>animate</code> variants, reduced-motion
87+
fallback, pixel-identical rest state). Paste it into any LLM to author a matching icon or
88+
restyle this one.
8489
</p>
8590

8691
<h2>TypeScript</h2>

app/globals.css

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,32 @@ body {
656656
the CTA's 40px top margin = the same 72px rhythm. */
657657
padding: 0 0 32px;
658658
}
659+
/* The set enters only after the hero entrance has played, so the intro reads
660+
hero-first, grid-second. Pure CSS (like .fh-rise) rather than Motion: it
661+
paints and plays at first paint instead of waiting for hydration, and still
662+
runs with JS disabled. animation-fill-mode: backwards holds the `from` state
663+
through the delay, which is what keeps the grid out of the opening frame.
664+
The delay matches HERO_INTRO_SECONDS in app/page.tsx. The hero title stays
665+
the LCP element, so gating this section's paint does not move LCP. */
666+
.dc-section--intro {
667+
animation: dc-section-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) 1.1s backwards;
668+
}
669+
@keyframes dc-section-in {
670+
from {
671+
opacity: 0;
672+
transform: translateY(16px);
673+
}
674+
to {
675+
opacity: 1;
676+
transform: none;
677+
}
678+
}
679+
@media (prefers-reduced-motion: reduce) {
680+
.dc-section--intro {
681+
animation: none;
682+
}
683+
}
684+
659685
.dc-section__head {
660686
display: flex;
661687
align-items: flex-end;
@@ -739,9 +765,6 @@ body {
739765
[data-reveal] {
740766
transform: translateY(12px);
741767
}
742-
.dc-grid > [data-reveal] {
743-
transform: translateY(12px) rotate(-2deg);
744-
}
745768
[data-reveal][data-revealed] {
746769
transform: none;
747770
transition: transform 0.45s cubic-bezier(0.34, 1.2, 0.64, 1) var(--stagger, 0s);
@@ -750,8 +773,7 @@ body {
750773
transition-delay: calc(var(--stagger, 0s) + 1.1s);
751774
}
752775
@media (prefers-reduced-motion: reduce) {
753-
[data-reveal],
754-
.dc-grid > [data-reveal] {
776+
[data-reveal] {
755777
transform: none;
756778
}
757779
[data-reveal][data-revealed] {

app/layout.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,41 @@ import { Analytics } from "@vercel/analytics/next";
55
import "./globals.css";
66
import { AppProvider } from "./providers";
77
import { StructuredData } from "@/components/seo/structured-data";
8-
import { SITE, SITE_NAME, TAGLINE, SITE_DESCRIPTION } from "@/lib/seo";
8+
import { SITE, SITE_NAME, META_TITLE, META_DESCRIPTION } from "@/lib/seo";
99

1010
/**
1111
* Vercel Geist typography: Geist Sans sets UI and prose, Geist Mono sets code,
1212
* data, and tabular figures. Both are open source and ship with the `geist`
1313
* package, so this is the real design system — no substitutes.
1414
*/
1515

16-
const TITLE = `${SITE_NAME}${TAGLINE}`;
16+
const TITLE = META_TITLE;
1717

1818
export const metadata: Metadata = {
1919
metadataBase: new URL(SITE),
2020
title: {
2121
default: TITLE,
2222
template: `%s · ${SITE_NAME}`,
2323
},
24-
description: SITE_DESCRIPTION,
24+
description: META_DESCRIPTION,
2525
applicationName: SITE_NAME,
26+
// Google has ignored the keywords meta tag since 2009, so this ranks nothing;
27+
// it is kept because some non-Google crawlers and AI indexers still read it,
28+
// and it costs a few bytes. The terms that actually have to earn rankings
29+
// live in the title, the description, and the on-page copy.
2630
keywords: [
31+
"animated icon library",
32+
"animated icons",
33+
"icon animation",
34+
"icon library",
35+
"icon motion",
36+
"motion icons",
2737
"animated react icons",
2838
"animated svg icons",
29-
"phosphor animated icons",
3039
"react icon library",
3140
"svg icon animation",
41+
"phosphor animated icons",
3242
"shadcn icons",
33-
"motion icons",
3443
"open source icons",
3544
],
3645
authors: [{ name: "Muhammad Ammar", url: "https://github.com/smammar100" }],
@@ -41,12 +50,12 @@ export const metadata: Metadata = {
4150
siteName: SITE_NAME,
4251
url: SITE,
4352
title: TITLE,
44-
description: SITE_DESCRIPTION,
53+
description: META_DESCRIPTION,
4554
},
4655
twitter: {
4756
card: "summary_large_image",
4857
title: TITLE,
49-
description: SITE_DESCRIPTION,
58+
description: META_DESCRIPTION,
5059
},
5160
robots: {
5261
index: true,

app/llms.txt/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ${KEY_FACTS.map((f) => `- ${f}`).join("\n")}
2121
- Framework: React 19
2222
- Design grid: Phosphor 256
2323
- Recommended render size: 24px
24-
- Distribution: shadcn registry, Open in v0
24+
- Distribution: shadcn registry, per-icon AI prompt (Copy AI prompt on any icon)
2525
- Animation model: spring physics with anticipation and settle frames, on hover and keyboard focus
2626
2727
## Links

0 commit comments

Comments
 (0)