From c99de0d9e8a74b16187fce42e0a13f614f6a66db Mon Sep 17 00:00:00 2001 From: Kai Date: Mon, 9 Feb 2026 20:20:07 -0800 Subject: [PATCH] feat: wire developer portal to persistent resource data --- data/developer-resources.json | 102 +++++ src/app/api/developer/route.ts | 119 ++++++ src/app/developer/page.tsx | 697 +++++++++++++-------------------- 3 files changed, 496 insertions(+), 422 deletions(-) create mode 100644 data/developer-resources.json create mode 100644 src/app/api/developer/route.ts diff --git a/data/developer-resources.json b/data/developer-resources.json new file mode 100644 index 00000000..4ad9aac2 --- /dev/null +++ b/data/developer-resources.json @@ -0,0 +1,102 @@ +[ + { + "id": "res-001", + "title": "OpenClaw TypeScript SDK", + "url": "https://github.com/openclaw-ai/openclaw-ts", + "type": "sdk", + "description": "Official TypeScript SDK for building, testing, and deploying OpenClaw-powered agents.", + "language": "TypeScript", + "stars": 1820, + "updatedAt": "2026-02-02T15:40:00.000Z" + }, + { + "id": "res-002", + "title": "OpenClaw Python SDK", + "url": "https://github.com/openclaw-ai/openclaw-py", + "type": "sdk", + "description": "Python client with async support, retries, and streaming primitives for agent workflows.", + "language": "Python", + "stars": 1460, + "updatedAt": "2026-01-30T11:22:00.000Z" + }, + { + "id": "res-003", + "title": "Agent Memory Toolkit", + "url": "https://github.com/foragents-dev/agent-memory-toolkit", + "type": "library", + "description": "Drop-in persistence, embeddings, and retrieval helpers for long-running autonomous agents.", + "language": "TypeScript", + "stars": 930, + "updatedAt": "2026-01-26T18:10:00.000Z" + }, + { + "id": "res-004", + "title": "Structured Prompting Utilities", + "url": "https://github.com/foragents-dev/structured-prompts", + "type": "library", + "description": "Composable prompt templates, validators, and schema-backed generation utilities.", + "language": "Python", + "stars": 680, + "updatedAt": "2026-01-18T09:45:00.000Z" + }, + { + "id": "res-005", + "title": "Slack Triage Bot Example", + "url": "https://github.com/foragents-dev/examples-slack-triage", + "type": "example", + "description": "End-to-end sample that triages support tickets from Slack into action queues.", + "language": "TypeScript", + "stars": 540, + "updatedAt": "2026-02-01T20:04:00.000Z" + }, + { + "id": "res-006", + "title": "GitHub PR Review Agent Example", + "url": "https://github.com/foragents-dev/examples-pr-review", + "type": "example", + "description": "Reference implementation for automated pull request reviews with policy checks.", + "language": "Go", + "stars": 790, + "updatedAt": "2026-02-04T07:12:00.000Z" + }, + { + "id": "res-007", + "title": "Building Reliable Agent Loops", + "url": "https://foragents.dev/guides/reliable-agent-loops", + "type": "tutorial", + "description": "Hands-on tutorial for retries, state checkpoints, and safe execution patterns.", + "language": "Markdown", + "stars": 420, + "updatedAt": "2026-01-29T13:32:00.000Z" + }, + { + "id": "res-008", + "title": "Observability for Multi-Agent Systems", + "url": "https://foragents.dev/guides/agent-observability", + "type": "tutorial", + "description": "Guide to tracing, metrics, and debugging workflows across distributed agent runtimes.", + "language": "Markdown", + "stars": 388, + "updatedAt": "2026-01-31T16:55:00.000Z" + }, + { + "id": "res-009", + "title": "Prompt Diff Inspector", + "url": "https://github.com/foragents-dev/prompt-diff-inspector", + "type": "tool", + "description": "CLI tool for comparing prompt revisions, outputs, and evaluation changes side-by-side.", + "language": "Rust", + "stars": 1015, + "updatedAt": "2026-02-03T22:18:00.000Z" + }, + { + "id": "res-010", + "title": "Agent Sandbox Runner", + "url": "https://github.com/foragents-dev/agent-sandbox-runner", + "type": "tool", + "description": "Secure local sandbox for replaying agent tasks with deterministic fixtures and logs.", + "language": "TypeScript", + "stars": 1240, + "updatedAt": "2026-02-05T10:06:00.000Z" + } +] diff --git a/src/app/api/developer/route.ts b/src/app/api/developer/route.ts new file mode 100644 index 00000000..7d9f823c --- /dev/null +++ b/src/app/api/developer/route.ts @@ -0,0 +1,119 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { NextResponse } from "next/server"; + +type ResourceType = "sdk" | "library" | "example" | "tutorial" | "tool"; + +interface DeveloperResource { + id: string; + title: string; + url: string; + type: ResourceType; + description: string; + language: string; + stars: number; + updatedAt: string; +} + +const VALID_TYPES: ResourceType[] = ["sdk", "library", "example", "tutorial", "tool"]; +const DATA_FILE = path.join(process.cwd(), "data", "developer-resources.json"); + +const readResources = async (): Promise => { + const file = await fs.readFile(DATA_FILE, "utf8"); + const parsed = JSON.parse(file) as DeveloperResource[]; + return Array.isArray(parsed) ? parsed : []; +}; + +const writeResources = async (resources: DeveloperResource[]) => { + await fs.writeFile(DATA_FILE, JSON.stringify(resources, null, 2), "utf8"); +}; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const typeParam = searchParams.get("type")?.toLowerCase(); + const searchParam = searchParams.get("search")?.toLowerCase().trim(); + + const resources = await readResources(); + + const filtered = resources.filter((resource) => { + const matchesType = !typeParam || typeParam === "all" || resource.type === typeParam; + + const matchesSearch = + !searchParam || + resource.title.toLowerCase().includes(searchParam) || + resource.description.toLowerCase().includes(searchParam) || + resource.language.toLowerCase().includes(searchParam); + + return matchesType && matchesSearch; + }); + + return NextResponse.json({ resources: filtered }); + } catch (error) { + console.error("Failed to load developer resources", error); + return NextResponse.json( + { error: "Failed to load developer resources" }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const body = (await request.json()) as Partial; + + const title = body.title?.trim(); + const url = body.url?.trim(); + const type = body.type; + const description = body.description?.trim(); + + if (!title || !url || !type || !description) { + return NextResponse.json( + { error: "title, url, type, and description are required" }, + { status: 400 } + ); + } + + if (!VALID_TYPES.includes(type)) { + return NextResponse.json( + { error: `type must be one of: ${VALID_TYPES.join(", ")}` }, + { status: 400 } + ); + } + + let parsedUrl: URL; + try { + parsedUrl = new URL(url); + } catch { + return NextResponse.json({ error: "url must be a valid URL" }, { status: 400 }); + } + + if (!["http:", "https:"].includes(parsedUrl.protocol)) { + return NextResponse.json({ error: "url must start with http:// or https://" }, { status: 400 }); + } + + const resources = await readResources(); + + const newResource: DeveloperResource = { + id: crypto.randomUUID(), + title, + url, + type, + description, + language: "Unknown", + stars: 0, + updatedAt: new Date().toISOString(), + }; + + resources.unshift(newResource); + await writeResources(resources); + + return NextResponse.json({ resource: newResource }, { status: 201 }); + } catch (error) { + console.error("Failed to create developer resource", error); + return NextResponse.json( + { error: "Failed to create developer resource" }, + { status: 500 } + ); + } +} diff --git a/src/app/developer/page.tsx b/src/app/developer/page.tsx index 880a0306..698db5e0 100644 --- a/src/app/developer/page.tsx +++ b/src/app/developer/page.tsx @@ -1,467 +1,320 @@ +/* eslint-disable react/no-unescaped-entities */ "use client"; -import { useState } from "react"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { FormEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { ExternalLink, Loader2, Star } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { Checkbox } from "@/components/ui/checkbox"; -import developerData from "@/data/developer.json"; +import { Textarea } from "@/components/ui/textarea"; -interface ApiKey { - id: string; - name: string; - key: string; - created: string; - lastUsed: string; - usage: { - today: number; - thisMonth: number; - rateLimit: { - limit: number; - remaining: number; - resetAt: string; - }; - }; -} +type ResourceType = "sdk" | "library" | "example" | "tutorial" | "tool"; -interface SDK { - name: string; - icon: string; - description: string; - installCommand: string; - version: string; - docsUrl: string; - exampleCode: string; -} - -interface QuickLink { +interface DeveloperResource { + id: string; title: string; url: string; - icon: string; + type: ResourceType; description: string; + language: string; + stars: number; + updatedAt: string; } -interface WebhookEvent { - name: string; - description: string; +interface ApiResponse { + resources: DeveloperResource[]; } -interface DeveloperData { - hero: { - title: string; - description: string; - quickLinks: QuickLink[]; - }; - apiKeys: ApiKey[]; - sdks: SDK[]; - webhooks: { - description: string; - events: WebhookEvent[]; - configured: { - url: string; - secret: string; - subscribedEvents: string[]; - }; - }; - quickStart: { - [key: string]: { - title: string; - code: string; - }; - }; -} +const RESOURCE_TYPES: ResourceType[] = ["sdk", "library", "example", "tutorial", "tool"]; -const data = developerData as DeveloperData; +const formatTypeLabel = (type: string) => type.charAt(0).toUpperCase() + type.slice(1); export default function DeveloperPage() { - const [copiedKey, setCopiedKey] = useState(null); - const [copiedCode, setCopiedCode] = useState(null); - const [showNewKeyForm, setShowNewKeyForm] = useState(false); - const [webhookUrl, setWebhookUrl] = useState(data.webhooks.configured.url); - const [selectedEvents, setSelectedEvents] = useState( - data.webhooks.configured.subscribedEvents - ); - - const maskApiKey = (key: string) => { - const prefix = key.substring(0, 12); - const suffix = key.substring(key.length - 4); - return `${prefix}${"•".repeat(20)}${suffix}`; - }; - - const copyToClipboard = async (text: string, type: "key" | "code", id: string) => { - await navigator.clipboard.writeText(text); - if (type === "key") { - setCopiedKey(id); - setTimeout(() => setCopiedKey(null), 2000); - } else { - setCopiedCode(id); - setTimeout(() => setCopiedCode(null), 2000); + const [resources, setResources] = useState([]); + const [typeFilter, setTypeFilter] = useState("all"); + const [search, setSearch] = useState(""); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + const [title, setTitle] = useState(""); + const [url, setUrl] = useState(""); + const [type, setType] = useState("sdk"); + const [description, setDescription] = useState(""); + const [submitMessage, setSubmitMessage] = useState(null); + + const fetchResources = useCallback(async () => { + setLoading(true); + setError(null); + + try { + const params = new URLSearchParams(); + if (typeFilter !== "all") params.set("type", typeFilter); + if (search.trim()) params.set("search", search.trim()); + + const response = await fetch(`/api/developer?${params.toString()}`, { + cache: "no-store", + }); + + if (!response.ok) { + throw new Error("Failed to load resources"); + } + + const data = (await response.json()) as ApiResponse; + setResources(data.resources); + } catch { + setError("Unable to load developer resources right now."); + } finally { + setLoading(false); } - }; - - const formatDate = (dateString: string) => { - return new Date(dateString).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - }; + }, [search, typeFilter]); + + useEffect(() => { + fetchResources(); + }, [fetchResources]); + + const groupedResources = useMemo(() => { + const groups: Record = { + sdk: [], + library: [], + example: [], + tutorial: [], + tool: [], + }; - const formatDateTime = (dateString: string) => { - return new Date(dateString).toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - }; + for (const resource of resources) { + groups[resource.type].push(resource); + } - const toggleEvent = (eventName: string) => { - setSelectedEvents((prev) => - prev.includes(eventName) - ? prev.filter((e) => e !== eventName) - : [...prev, eventName] - ); + return groups; + }, [resources]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setSubmitMessage(null); + setSubmitting(true); + + try { + const response = await fetch("/api/developer", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title, + url, + type, + description, + }), + }); + + const payload = (await response.json()) as { error?: string }; + + if (!response.ok) { + throw new Error(payload.error ?? "Failed to submit resource"); + } + + setTitle(""); + setUrl(""); + setType("sdk"); + setDescription(""); + setSubmitMessage("Resource submitted successfully."); + await fetchResources(); + } catch (submitError) { + if (submitError instanceof Error) { + setSubmitMessage(submitError.message); + } else { + setSubmitMessage("Failed to submit resource."); + } + } finally { + setSubmitting(false); + } }; return ( -
- {/* Hero Section */} -
-

{data.hero.title}

-

- {data.hero.description} +

+
+

Developer Resources

+

+ Browse SDKs, libraries, tutorials, tools, and examples shared by the forAgents community. + Here's what's new and useful for your next build.

- -
- {data.hero.quickLinks.map((link) => ( - - - - {link.icon} - {link.title} - - - -

{link.description}

-
-
- ))} -
-
- - - - {/* API Keys Section */} -
-
-
-

API Keys

-

- Manage your API keys and monitor usage -

+ + + + + Filter Resources + + +
+ +
- -
- {showNewKeyForm && ( - - - Create New API Key - - -
-
- - -
-
- - -
-
-
-
- )} - -
- {data.apiKeys.map((apiKey) => ( - - -
-
- {apiKey.name} -

- Created {formatDate(apiKey.created)} • Last used{" "} - {formatDateTime(apiKey.lastUsed)} -

-
- -
-
- -
-
- -
- - {maskApiKey(apiKey.key)} - - -
-
- -
-
- -

- {apiKey.usage.today.toLocaleString()} -

-
-
- -

- {apiKey.usage.thisMonth.toLocaleString()} -

-
-
- -

- {apiKey.usage.rateLimit.remaining.toLocaleString()} /{" "} - {apiKey.usage.rateLimit.limit.toLocaleString()} -

-

- Resets {formatDateTime(apiKey.usage.rateLimit.resetAt)} -

-
-
-
-
-
- ))} -
-
- - +
+ + setSearch(event.target.value)} + /> +
+ + - {/* SDK Downloads Section */} -
-
-

SDKs & Libraries

-

- Official client libraries for your favorite languages -

+ {loading && ( +
+ + Loading resources...
+ )} -
- {data.sdks.map((sdk) => ( - - -
- - {sdk.icon} - {sdk.name} - - v{sdk.version} -
-

{sdk.description}

-
- -
-
- -
- - {sdk.installCommand} - - -
-
-
- -
-
-                        {sdk.exampleCode}
-                      
- -
-
- + {!loading && error && ( + + {error} + + )} + + {!loading && !error && ( +
+ {RESOURCE_TYPES.map((resourceType) => { + const items = groupedResources[resourceType]; + if (items.length === 0) return null; + + return ( +
+

{formatTypeLabel(resourceType)}

+
+ {items.map((resource) => ( + + + + {resource.title} + + + + +

{resource.description}

+
+ {resource.language} + + + {resource.stars.toLocaleString()} + + Updated {new Date(resource.updatedAt).toLocaleDateString()} +
+
+
+ ))}
+
+ ); + })} + + {resources.length === 0 && ( + + + No resources found for the selected filters. - ))} + )}
-
- - + )} + + + + + + Submit Resource + + +
+
+ + setTitle(event.target.value)} + placeholder="Resource name" + required + /> +
- {/* Webhooks Section */} -
-
-

Webhooks

-

{data.webhooks.description}

-
+
+ + setUrl(event.target.value)} + placeholder="https://example.com/resource" + required + /> +
- - - Webhook Configuration - - -
-
- - setWebhookUrl(e.target.value)} - placeholder="https://api.example.com/webhooks/foragents" - className="mt-2" - /> -
- -
- -
- - {maskApiKey(data.webhooks.configured.secret)} - - - -
-
- -
- -
- {data.webhooks.events.map((event) => ( -
- toggleEvent(event.name)} - /> -
- -

- {event.description} -

-
-
+
+ +
- - -
- +
+ +