Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions data/spaces.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
[
{
"id": "space_001",
"name": "Open Source Toolsmiths",
"slug": "open-source-toolsmiths",
"description": "Builders shipping reusable tooling for agent-first workflows and open collaboration.",
"visibility": "public",
"memberCount": 324,
"skillCount": 48,
"createdBy": "@kai",
"createdAt": "2025-10-12T14:24:00.000Z",
"tags": ["open-source", "tooling", "automation"],
"featured": true
},
{
"id": "space_002",
"name": "Enterprise Automation Guild",
"slug": "enterprise-automation-guild",
"description": "Private collaboration space for teams deploying AI agents in production enterprise systems.",
"visibility": "private",
"memberCount": 167,
"skillCount": 37,
"createdBy": "@ryan",
"createdAt": "2025-11-04T09:10:00.000Z",
"tags": ["enterprise", "security", "governance"],
"featured": true
},
{
"id": "space_003",
"name": "Research Signals Lab",
"slug": "research-signals-lab",
"description": "Tracking model behavior, benchmarks, and evaluation patterns across agent ecosystems.",
"visibility": "public",
"memberCount": 289,
"skillCount": 62,
"createdBy": "@mira",
"createdAt": "2025-09-20T18:45:00.000Z",
"tags": ["research", "evaluation", "benchmarks"],
"featured": false
},
{
"id": "space_004",
"name": "Prompt Architecture Circle",
"slug": "prompt-architecture-circle",
"description": "Unlisted workspace for refining reusable prompt systems, templates, and testing harnesses.",
"visibility": "unlisted",
"memberCount": 94,
"skillCount": 29,
"createdBy": "@nova",
"createdAt": "2025-12-01T12:32:00.000Z",
"tags": ["prompts", "templates", "testing"],
"featured": false
},
{
"id": "space_005",
"name": "Builder Launch Crew",
"slug": "builder-launch-crew",
"description": "A public hub for shipping launches, collecting feedback, and iterating quickly in public.",
"visibility": "public",
"memberCount": 451,
"skillCount": 55,
"createdBy": "@echo",
"createdAt": "2025-08-28T07:55:00.000Z",
"tags": ["launch", "feedback", "community"],
"featured": true
},
{
"id": "space_006",
"name": "Agent UX Studio",
"slug": "agent-ux-studio",
"description": "Designers and builders co-creating smooth human-agent interfaces and interaction patterns.",
"visibility": "public",
"memberCount": 211,
"skillCount": 44,
"createdBy": "@lyra",
"createdAt": "2025-10-30T16:20:00.000Z",
"tags": ["ux", "design", "product"],
"featured": false
},
{
"id": "space_007",
"name": "Infra Reliability Room",
"slug": "infra-reliability-room",
"description": "Private ops-focused space for incident playbooks, observability, and deployment reliability.",
"visibility": "private",
"memberCount": 73,
"skillCount": 26,
"createdBy": "@atlas",
"createdAt": "2026-01-15T03:40:00.000Z",
"tags": ["infrastructure", "observability", "ops"],
"featured": false
},
{
"id": "space_008",
"name": "Education Sandbox",
"slug": "education-sandbox",
"description": "Unlisted learning community for onboarding new builders with hands-on agent projects.",
"visibility": "unlisted",
"memberCount": 129,
"skillCount": 33,
"createdBy": "@sage",
"createdAt": "2025-11-22T21:05:00.000Z",
"tags": ["education", "onboarding", "mentorship"],
"featured": true
}
]
117 changes: 117 additions & 0 deletions src/app/api/spaces/[slug]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { NextRequest, NextResponse } from "next/server";
import { promises as fs } from "node:fs";
import path from "node:path";

type SpaceVisibility = "public" | "private" | "unlisted";

type Space = {
id: string;
name: string;
slug: string;
description: string;
visibility: SpaceVisibility;
memberCount: number;
skillCount: number;
createdBy: string;
createdAt: string;
tags: string[];
featured: boolean;
};

const SPACES_PATH = path.join(process.cwd(), "data", "spaces.json");

function isVisibility(value: string): value is SpaceVisibility {
return value === "public" || value === "private" || value === "unlisted";
}

function normalizeSpaces(data: unknown): Space[] {
if (!Array.isArray(data)) return [];

return data.filter(
(space): space is Space =>
typeof space?.id === "string" &&
typeof space?.name === "string" &&
typeof space?.slug === "string" &&
typeof space?.description === "string" &&
typeof space?.visibility === "string" &&
isVisibility(space.visibility) &&
typeof space?.memberCount === "number" &&
typeof space?.skillCount === "number" &&
typeof space?.createdBy === "string" &&
typeof space?.createdAt === "string" &&
Array.isArray(space?.tags) &&
space.tags.every((tag: unknown) => typeof tag === "string") &&
typeof space?.featured === "boolean"
);
}

async function readSpaces(): Promise<Space[]> {
try {
const raw = await fs.readFile(SPACES_PATH, "utf8");
return normalizeSpaces(JSON.parse(raw));
} catch {
return [];
}
}

async function writeSpaces(spaces: Space[]) {
await fs.writeFile(SPACES_PATH, JSON.stringify(spaces, null, 2));
}

export async function GET(_request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const targetSlug = typeof slug === "string" ? slug.trim() : "";

if (!targetSlug) {
return NextResponse.json({ error: "Space slug is required." }, { status: 400 });
}

const spaces = await readSpaces();
const space = spaces.find((item) => item.slug === targetSlug);

if (!space) {
return NextResponse.json({ error: "Space not found." }, { status: 404 });
}

return NextResponse.json({ space }, { headers: { "Cache-Control": "no-store" } });
}

export async function POST(request: NextRequest, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const targetSlug = typeof slug === "string" ? slug.trim() : "";

if (!targetSlug) {
return NextResponse.json({ error: "Space slug is required." }, { status: 400 });
}

const body = (await request.json().catch(() => null)) as { action?: unknown } | null;
const action = typeof body?.action === "string" ? body.action.trim().toLowerCase() : "join";

if (action !== "join" && action !== "leave") {
return NextResponse.json({ error: "action must be join or leave." }, { status: 400 });
}

const spaces = await readSpaces();
const index = spaces.findIndex((item) => item.slug === targetSlug);

if (index === -1) {
return NextResponse.json({ error: "Space not found." }, { status: 404 });
}

const existing = spaces[index];
const memberCount = action === "join" ? existing.memberCount + 1 : Math.max(0, existing.memberCount - 1);

const updatedSpace: Space = {
...existing,
memberCount,
};

spaces[index] = updatedSpace;
await writeSpaces(spaces);

return NextResponse.json({
success: true,
action,
space: updatedSpace,
});
}
Loading
Loading