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
86 changes: 86 additions & 0 deletions data/contribution-guides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[
{
"id": "guide-skills-001",
"title": "Publish a High-Impact Skill",
"description": "Create or improve a reusable skill so agents can solve a task faster with better guardrails.",
"category": "skills",
"difficulty": "intermediate",
"estimatedTime": "2-4 hours",
"steps": [
"Choose a repeatable workflow that agents struggle with.",
"Draft the skill contract, inputs, outputs, and failure modes.",
"Add practical examples and edge-case handling.",
"Test the skill with at least one real task before submitting."
]
},
{
"id": "guide-docs-001",
"title": "Improve Core Documentation",
"description": "Tighten docs so new contributors can onboard quickly and avoid common mistakes.",
"category": "docs",
"difficulty": "beginner",
"estimatedTime": "1-2 hours",
"steps": [
"Pick one doc page with confusing or outdated instructions.",
"Rewrite sections for clarity and add concrete examples.",
"Verify commands and links still work.",
"Submit your changes with a brief before/after summary."
]
},
{
"id": "guide-testing-001",
"title": "Add Regression Test Coverage",
"description": "Strengthen reliability by covering known bugs and critical user paths.",
"category": "testing",
"difficulty": "advanced",
"estimatedTime": "3-5 hours",
"steps": [
"Identify a flaky or high-risk area from recent issues.",
"Write focused tests that fail before your fix.",
"Implement the minimal fix and verify tests pass.",
"Document why this regression test matters."
]
},
{
"id": "guide-design-001",
"title": "Refine UX for Contributor Flows",
"description": "Polish visual hierarchy and interaction details to make contribution workflows easier.",
"category": "design",
"difficulty": "intermediate",
"estimatedTime": "2-3 hours",
"steps": [
"Audit one contributor flow and list friction points.",
"Propose UI changes with accessibility in mind.",
"Validate spacing, contrast, and responsive behavior.",
"Ship the update with screenshots and rationale."
]
},
{
"id": "guide-translations-001",
"title": "Translate Getting Started Content",
"description": "Expand access by translating key onboarding and contribution content.",
"category": "translations",
"difficulty": "beginner",
"estimatedTime": "1-3 hours",
"steps": [
"Select a target language and source page.",
"Translate text while preserving technical meaning.",
"Flag culture-specific terms that may need adaptation.",
"Submit translation notes for reviewer context."
]
},
{
"id": "guide-community-001",
"title": "Support Community Triage",
"description": "Help route community requests, answer common questions, and keep discussions actionable.",
"category": "community",
"difficulty": "beginner",
"estimatedTime": "30-90 minutes",
"steps": [
"Review unanswered requests and duplicate topics.",
"Respond with clear next steps or resource links.",
"Label escalations for maintainers when required.",
"Summarize outcomes so others can continue the thread."
]
}
]
42 changes: 42 additions & 0 deletions data/contributions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"id": "contrib-001",
"contributorName": "Ari Kim",
"type": "docs",
"title": "Clarified bootstrap troubleshooting section",
"status": "merged",
"submittedAt": "2026-02-05T18:30:00.000Z"
},
{
"id": "contrib-002",
"contributorName": "Mateo Singh",
"type": "testing",
"title": "Added route-level tests for API error handling",
"status": "approved",
"submittedAt": "2026-02-06T09:15:00.000Z"
},
{
"id": "contrib-003",
"contributorName": "Lina Park",
"type": "design",
"title": "Updated contribution cards for mobile readability",
"status": "pending",
"submittedAt": "2026-02-06T22:05:00.000Z"
},
{
"id": "contrib-004",
"contributorName": "Owen Patel",
"type": "skills",
"title": "Published skill template for CLI-based workflows",
"status": "merged",
"submittedAt": "2026-02-07T14:42:00.000Z"
},
{
"id": "contrib-005",
"contributorName": "Nora Alvarez",
"type": "community",
"title": "Triaged onboarding questions into common-answer pack",
"status": "pending",
"submittedAt": "2026-02-08T01:20:00.000Z"
}
]
154 changes: 154 additions & 0 deletions src/app/api/contribute/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { promises as fs } from "fs";
import path from "path";
import { NextRequest, NextResponse } from "next/server";

type GuideCategory = "skills" | "docs" | "testing" | "design" | "translations" | "community";
type GuideDifficulty = "beginner" | "intermediate" | "advanced";
type ContributionStatus = "pending" | "approved" | "merged";

type ContributionGuide = {
id: string;
title: string;
description: string;
category: GuideCategory;
difficulty: GuideDifficulty;
estimatedTime: string;
steps: string[];
};

type Contribution = {
id: string;
contributorName: string;
type: string;
title: string;
status: ContributionStatus;
submittedAt: string;
};

const GUIDES_PATH = path.join(process.cwd(), "data", "contribution-guides.json");
const CONTRIBUTIONS_PATH = path.join(process.cwd(), "data", "contributions.json");
const VALID_TYPES: GuideCategory[] = ["skills", "docs", "testing", "design", "translations", "community"];

async function readGuides(): Promise<ContributionGuide[]> {
const raw = await fs.readFile(GUIDES_PATH, "utf-8");
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? (parsed as ContributionGuide[]) : [];
}

async function readContributions(): Promise<Contribution[]> {
try {
const raw = await fs.readFile(CONTRIBUTIONS_PATH, "utf-8");
const parsed = JSON.parse(raw) as unknown;
return Array.isArray(parsed) ? (parsed as Contribution[]) : [];
} catch {
return [];
}
}

async function writeContributions(contributions: Contribution[]) {
await fs.mkdir(path.dirname(CONTRIBUTIONS_PATH), { recursive: true });
const tmpPath = `${CONTRIBUTIONS_PATH}.tmp`;
await fs.writeFile(tmpPath, JSON.stringify(contributions, null, 2), "utf-8");
await fs.rename(tmpPath, CONTRIBUTIONS_PATH);
}

function isValidType(type: string): type is GuideCategory {
return VALID_TYPES.includes(type as GuideCategory);
}

function normalizeTitle(title: string, description: string) {
if (title.trim()) {
return title.trim().slice(0, 120);
}

const fallback = description.trim().slice(0, 80);
return fallback.length > 0 ? fallback : "New contribution";
}

export async function GET() {
try {
const [guides, contributions] = await Promise.all([readGuides(), readContributions()]);

const recentContributions = [...contributions]
.sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
.slice(0, 12);

return NextResponse.json(
{
guides,
recentContributions,
},
{
headers: {
"Cache-Control": "no-store",
},
}
);
} catch {
return NextResponse.json({ error: "Failed to load contribution data." }, { status: 500 });
}
}

export async function POST(request: NextRequest) {
try {
const body = (await request.json()) as Record<string, unknown>;

const name = typeof body.name === "string" ? body.name.trim() : "";
const email = typeof body.email === "string" ? body.email.trim() : "";
const typeRaw = typeof body.type === "string" ? body.type.trim().toLowerCase() : "";
const description = typeof body.description === "string" ? body.description.trim() : "";
const title = typeof body.title === "string" ? body.title : "";

if (!name || !email || !typeRaw || !description) {
return NextResponse.json(
{
error: "Validation failed",
details: "name, email, type, and description are required.",
},
{ status: 400 }
);
}

if (!/^\S+@\S+\.\S+$/.test(email)) {
return NextResponse.json(
{
error: "Validation failed",
details: "email must be a valid email address.",
},
{ status: 400 }
);
}

if (!isValidType(typeRaw)) {
return NextResponse.json(
{
error: "Validation failed",
details: `type must be one of: ${VALID_TYPES.join(", ")}`,
},
{ status: 400 }
);
}

const contribution: Contribution = {
id: `contrib_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
contributorName: name,
type: typeRaw,
title: normalizeTitle(title, description),
status: "pending",
submittedAt: new Date().toISOString(),
};

const existing = await readContributions();
await writeContributions([contribution, ...existing]);

return NextResponse.json(
{
message: "Contribution submitted successfully.",
contribution,
},
{ status: 201 }
);
} catch {
return NextResponse.json({ error: "Invalid request body. Expected JSON." }, { status: 400 });
}
}
Loading
Loading