Compare commits
2 Commits
b3c375d26d
...
79dad6124f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79dad6124f | ||
| efdc282da5 |
5
apps/web/next-env.d.ts
vendored
Normal file
5
apps/web/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||||
7
apps/web/next.config.mjs
Normal file
7
apps/web/next.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const config = {
|
||||||
|
transpilePackages: ["@codeboard/shared"],
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
39
apps/web/src/app/api/generate/route.ts
Normal file
39
apps/web/src/app/api/generate/route.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getQueue } from "@/lib/queue";
|
||||||
|
import { getRedis } from "@/lib/redis";
|
||||||
|
|
||||||
|
const GITHUB_URL_RE = /^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const body = await request.json();
|
||||||
|
const repoUrl: string = body.repoUrl?.trim();
|
||||||
|
|
||||||
|
if (!repoUrl || !GITHUB_URL_RE.test(repoUrl)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid GitHub repository URL" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generationId = `gen_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
|
||||||
|
const redis = getRedis();
|
||||||
|
await redis.set(
|
||||||
|
`codeboard:status:${generationId}`,
|
||||||
|
JSON.stringify({ status: "QUEUED", progress: 0, message: "Queued..." }),
|
||||||
|
"EX",
|
||||||
|
3600
|
||||||
|
);
|
||||||
|
|
||||||
|
const queue = getQueue();
|
||||||
|
await queue.add("generate", { repoUrl, generationId }, {
|
||||||
|
jobId: generationId,
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ id: generationId, status: "QUEUED" },
|
||||||
|
{ status: 202 }
|
||||||
|
);
|
||||||
|
}
|
||||||
68
apps/web/src/app/api/status/[id]/route.ts
Normal file
68
apps/web/src/app/api/status/[id]/route.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { getRedis } from "@/lib/redis";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const { id } = await params;
|
||||||
|
const redis = getRedis();
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const send = (event: string, data: unknown) => {
|
||||||
|
controller.enqueue(
|
||||||
|
encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sub = redis.duplicate();
|
||||||
|
const channel = `codeboard:progress:${id}`;
|
||||||
|
|
||||||
|
const currentStatus = await redis.get(`codeboard:status:${id}`);
|
||||||
|
if (currentStatus) {
|
||||||
|
const parsed = JSON.parse(currentStatus);
|
||||||
|
send("progress", parsed);
|
||||||
|
|
||||||
|
if (parsed.status === "COMPLETED" || parsed.status === "FAILED") {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await sub.subscribe(channel);
|
||||||
|
|
||||||
|
sub.on("message", (_ch: string, message: string) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
send("progress", data);
|
||||||
|
|
||||||
|
if (data.status === "COMPLETED" || data.status === "FAILED") {
|
||||||
|
sub.unsubscribe(channel);
|
||||||
|
sub.quit();
|
||||||
|
controller.close();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore parse errors */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
sub.unsubscribe(channel);
|
||||||
|
sub.quit();
|
||||||
|
send("timeout", { message: "Connection timed out" });
|
||||||
|
controller.close();
|
||||||
|
}, 300_000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { DocViewer } from "@/components/doc-viewer";
|
||||||
import type { GeneratedDocs } from "@codeboard/shared";
|
import type { GeneratedDocs } from "@codeboard/shared";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Github, ArrowLeft } from "lucide-react";
|
import { Github, ArrowLeft } from "lucide-react";
|
||||||
@@ -34,21 +35,30 @@ export default async function DocsPage({
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<div className="border-b border-white/10 bg-black/20">
|
<div className="border-b border-white/10 bg-black/20">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="text-center mb-12">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-white">
|
<Link
|
||||||
Analyzing Repository
|
href="/"
|
||||||
</h1>
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
<p className="text-zinc-400 max-w-xl mx-auto">
|
>
|
||||||
{docs.repoName || repoUrl || "Unknown repository"}
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</p>
|
Back to Home
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={docs.repoUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative inline-flex items-center justify-center w-16 h-16 rounded-2xl glass border-white/10">
|
<DocViewer docs={docs} />
|
||||||
<Github className="w-6 h-6 text-zinc-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ function GeneratePageSkeleton() {
|
|||||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6">
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6">
|
||||||
<Loader2 className="w-8 h-8 text-zinc-400 animate-spin" />
|
<Loader2 className="w-8 h-8 text-zinc-400 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-8 w-48 bg-zinc-800 rounded animate-pulse mx-auto mb-3" />
|
<div className="h-8 w-48 bg-zinc-800 rounded animate-pulse mx-auto mb-3" />
|
||||||
<div className="h-4 w-64 bg-zinc-800 rounded animate-pulse mx-auto" />
|
<div className="h-4 w-64 bg-zinc-800 rounded animate-pulse mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ body {
|
|||||||
.input-field:focus {
|
.input-field:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-blue);
|
border-color: var(--accent-blue);
|
||||||
box-shadow: 0 0 3px rgba(59, 130, 246, 0.1);
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field::placeholder {
|
.input-field::placeholder {
|
||||||
|
|||||||
280
apps/web/src/app/page.tsx
Normal file
280
apps/web/src/app/page.tsx
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { RepoInput } from "@/components/repo-input";
|
||||||
|
import {
|
||||||
|
Link2,
|
||||||
|
Code2,
|
||||||
|
Sparkles,
|
||||||
|
FileText,
|
||||||
|
GitBranch,
|
||||||
|
Boxes,
|
||||||
|
Search,
|
||||||
|
BookOpen,
|
||||||
|
ArrowRight,
|
||||||
|
Github
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
number: "01",
|
||||||
|
icon: Link2,
|
||||||
|
title: "Paste URL",
|
||||||
|
description: "Enter any public GitHub repository URL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "02",
|
||||||
|
icon: Code2,
|
||||||
|
title: "Clone & Analyze",
|
||||||
|
description: "We clone and deeply analyze the codebase structure",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "03",
|
||||||
|
icon: Sparkles,
|
||||||
|
title: "AI Generation",
|
||||||
|
description: "Our AI generates comprehensive documentation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "04",
|
||||||
|
icon: FileText,
|
||||||
|
title: "Interactive Docs",
|
||||||
|
description: "Explore architecture diagrams and module breakdowns",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: GitBranch,
|
||||||
|
title: "Architecture Diagrams",
|
||||||
|
description:
|
||||||
|
"Auto-generated Mermaid diagrams visualizing your codebase structure, dependencies, and data flow.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Boxes,
|
||||||
|
title: "Module Breakdowns",
|
||||||
|
description:
|
||||||
|
"Understand each part of the codebase with detailed summaries, key files, and public APIs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Search,
|
||||||
|
title: "Pattern Detection",
|
||||||
|
description:
|
||||||
|
"Coding conventions and design patterns automatically identified and documented for you.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BookOpen,
|
||||||
|
title: "Getting Started Guide",
|
||||||
|
description:
|
||||||
|
"Actionable onboarding documentation to get new developers productive in minutes, not days.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="relative pt-32 pb-20 lg:pt-48 lg:pb-32">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full glass mb-8 animate-fade-in opacity-0">
|
||||||
|
<Sparkles className="w-4 h-4 text-blue-400" />
|
||||||
|
<span className="text-sm text-zinc-300">Powered by AI</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Headline */}
|
||||||
|
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold tracking-tight mb-6 animate-slide-up opacity-0">
|
||||||
|
<span className="gradient-text">Understand any codebase</span>
|
||||||
|
<br />
|
||||||
|
<span className="text-white">in 5 minutes</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<p className="text-lg sm:text-xl text-zinc-400 max-w-2xl mx-auto mb-10 animate-slide-up opacity-0 stagger-1">
|
||||||
|
Paste a GitHub URL. Get interactive onboarding documentation with
|
||||||
|
architecture diagrams, module breakdowns, and getting started guides.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Repo Input */}
|
||||||
|
<div className="max-w-xl mx-auto mb-16 animate-slide-up opacity-0 stagger-2">
|
||||||
|
<RepoInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="flex flex-wrap justify-center gap-8 sm:gap-12 animate-fade-in opacity-0 stagger-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-white">5 min</div>
|
||||||
|
<div className="text-sm text-zinc-500">Average generation time</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-white">100%</div>
|
||||||
|
<div className="text-sm text-zinc-500">Free for public repos</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block w-px bg-zinc-800" />
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-white">AI</div>
|
||||||
|
<div className="text-sm text-zinc-500">Powered insights</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Background Elements */}
|
||||||
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] bg-blue-500/10 rounded-full blur-3xl pointer-events-none" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* How It Works Section */}
|
||||||
|
<section id="how-it-works" className="py-20 lg:py-32">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||||
|
How It Works
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 max-w-xl mx-auto">
|
||||||
|
Four simple steps to comprehensive codebase documentation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{/* Connection Line - Desktop */}
|
||||||
|
<div className="hidden lg:block absolute top-24 left-[12.5%] right-[12.5%] h-px bg-gradient-to-r from-transparent via-zinc-700 to-transparent" />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<div
|
||||||
|
key={step.number}
|
||||||
|
className="relative group"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
{/* Step Number */}
|
||||||
|
<div className="text-6xl font-bold text-zinc-800/50 mb-4 group-hover:text-blue-500/20 transition-colors">
|
||||||
|
{step.number}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="relative inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6 group-hover:border-blue-500/30 transition-colors">
|
||||||
|
<step.icon className="w-7 h-7 text-blue-400" />
|
||||||
|
|
||||||
|
{/* Glow effect */}
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-blue-500/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-2">
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm text-zinc-400 leading-relaxed">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
<section id="features" className="py-20 lg:py-32">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||||
|
Everything You Need
|
||||||
|
</h2>
|
||||||
|
<p className="text-zinc-400 max-w-xl mx-auto">
|
||||||
|
Comprehensive documentation generated automatically from your codebase
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<div
|
||||||
|
key={feature.title}
|
||||||
|
className="group relative p-8 rounded-2xl glass hover:bg-white/[0.05] transition-all duration-300 hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
{/* Gradient border on hover */}
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-r from-blue-500/20 via-indigo-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity -z-10 blur-xl" />
|
||||||
|
|
||||||
|
<div className="flex items-start gap-5">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center border border-white/10 group-hover:border-blue-500/30 transition-colors">
|
||||||
|
<feature.icon className="w-6 h-6 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-2 group-hover:text-blue-300 transition-colors">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-zinc-400 leading-relaxed">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Built by Vectry Section */}
|
||||||
|
<section className="py-20 lg:py-32">
|
||||||
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="relative rounded-3xl glass-strong p-8 sm:p-12 lg:p-16 overflow-hidden">
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-48 h-48 bg-gradient-to-tr from-indigo-500/10 to-cyan-500/10 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2" />
|
||||||
|
|
||||||
|
<div className="relative text-center">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-8">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse" />
|
||||||
|
<span className="text-sm text-zinc-300">Available for projects</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||||
|
Built by{" "}
|
||||||
|
<span className="gradient-text">Vectry</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-lg text-zinc-400 mb-4 max-w-xl mx-auto">
|
||||||
|
We're an AI consultancy that builds tools like this for businesses.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-zinc-500 mb-8">
|
||||||
|
Need AI automation for your workflow?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://company.repi.fun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 btn-primary animate-pulse-glow"
|
||||||
|
>
|
||||||
|
Talk to Us
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div className="mt-12 pt-8 border-t border-white/10">
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-6 text-sm text-zinc-500">
|
||||||
|
<a
|
||||||
|
href="https://github.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
Open Source
|
||||||
|
</a>
|
||||||
|
<span className="hidden sm:inline">•</span>
|
||||||
|
<span>Free for public repositories</span>
|
||||||
|
<span className="hidden sm:inline">•</span>
|
||||||
|
<span>No signup required</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
354
apps/web/src/components/doc-viewer.tsx
Normal file
354
apps/web/src/components/doc-viewer.tsx
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { GeneratedDocs, DocsModule } from "@codeboard/shared";
|
||||||
|
import { MermaidDiagram } from "./mermaid-diagram";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
Boxes,
|
||||||
|
Search,
|
||||||
|
Rocket,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Folder,
|
||||||
|
FileCode,
|
||||||
|
GitBranch,
|
||||||
|
ExternalLink,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface DocViewerProps {
|
||||||
|
docs: GeneratedDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocViewer({ docs }: DocViewerProps) {
|
||||||
|
const [activeSection, setActiveSection] = useState("overview");
|
||||||
|
const [expandedModules, setExpandedModules] = useState<Set<string>>(
|
||||||
|
new Set()
|
||||||
|
);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
const toggleModule = (moduleName: string) => {
|
||||||
|
const newSet = new Set(expandedModules);
|
||||||
|
if (newSet.has(moduleName)) {
|
||||||
|
newSet.delete(moduleName);
|
||||||
|
} else {
|
||||||
|
newSet.add(moduleName);
|
||||||
|
}
|
||||||
|
setExpandedModules(newSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToSection = (sectionId: string) => {
|
||||||
|
setActiveSection(sectionId);
|
||||||
|
const element = document.getElementById(sectionId);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ id: "overview", label: "Overview", icon: BookOpen },
|
||||||
|
{ id: "modules", label: "Modules", icon: Boxes },
|
||||||
|
{ id: "patterns", label: "Patterns", icon: Search },
|
||||||
|
{ id: "getting-started", label: "Getting Started", icon: Rocket },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
||||||
|
className="lg:hidden flex items-center gap-2 px-4 py-2 glass rounded-lg text-sm"
|
||||||
|
>
|
||||||
|
<BookOpen className="w-4 h-4" />
|
||||||
|
Table of Contents
|
||||||
|
{isSidebarOpen ? (
|
||||||
|
<ChevronDown className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
className={`${
|
||||||
|
isSidebarOpen ? "block" : "hidden"
|
||||||
|
} lg:block w-full lg:w-64 flex-shrink-0`}
|
||||||
|
>
|
||||||
|
<div className="sticky top-24 space-y-1">
|
||||||
|
<p className="px-3 py-2 text-xs font-semibold text-zinc-500 uppercase tracking-wider">
|
||||||
|
Contents
|
||||||
|
</p>
|
||||||
|
{sections.map((section) => (
|
||||||
|
<button
|
||||||
|
key={section.id}
|
||||||
|
onClick={() => scrollToSection(section.id)}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-left ${
|
||||||
|
activeSection === section.id
|
||||||
|
? "bg-blue-500/20 text-blue-300"
|
||||||
|
: "text-zinc-400 hover:text-white hover:bg-white/5"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<section.icon className="w-4 h-4" />
|
||||||
|
{section.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="pt-4 mt-4 border-t border-white/10">
|
||||||
|
<p className="px-3 py-2 text-xs font-semibold text-zinc-500 uppercase tracking-wider">
|
||||||
|
Repository
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={docs.repoUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
View on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="flex-1 min-w-0 space-y-16">
|
||||||
|
<div className="border-b border-white/10 pb-8">
|
||||||
|
<h1 className="text-3xl sm:text-4xl font-bold text-white mb-4">
|
||||||
|
{docs.sections.overview.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-zinc-400">
|
||||||
|
{docs.sections.overview.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-4">
|
||||||
|
{docs.sections.overview.techStack.map((tech: string) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="px-3 py-1 text-sm bg-white/5 border border-white/10 rounded-full text-zinc-300"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="overview" className="scroll-mt-24">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||||
|
<BookOpen className="w-6 h-6 text-blue-400" />
|
||||||
|
Architecture Overview
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="glass rounded-xl p-6 mb-6">
|
||||||
|
<MermaidDiagram chart={docs.sections.overview.architectureDiagram} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="p-4 glass rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{docs.sections.overview.keyMetrics.files}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-zinc-500">Files</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 glass rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{docs.sections.overview.keyMetrics.modules}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-zinc-500">Modules</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 glass rounded-lg text-center">
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{docs.sections.overview.keyMetrics.languages.length}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-zinc-500">Languages</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="modules" className="scroll-mt-24">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||||
|
<Boxes className="w-6 h-6 text-blue-400" />
|
||||||
|
Module Breakdown
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{docs.sections.modules.map((module: DocsModule) => (
|
||||||
|
<div
|
||||||
|
key={module.name}
|
||||||
|
className="glass rounded-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleModule(module.name)}
|
||||||
|
className="w-full flex items-center justify-between p-5 hover:bg-white/[0.02] transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Folder className="w-5 h-5 text-blue-400" />
|
||||||
|
<div className="text-left">
|
||||||
|
<h3 className="font-semibold text-white">{module.name}</h3>
|
||||||
|
<p className="text-sm text-zinc-500">{module.path}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expandedModules.has(module.name) ? (
|
||||||
|
<ChevronDown className="w-5 h-5 text-zinc-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-5 h-5 text-zinc-400" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{expandedModules.has(module.name) && (
|
||||||
|
<div className="px-5 pb-5 border-t border-white/10">
|
||||||
|
<p className="text-zinc-300 mt-4 mb-4">{module.summary}</p>
|
||||||
|
|
||||||
|
{module.keyFiles.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="text-sm font-medium text-zinc-400 mb-2">
|
||||||
|
Key Files
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{module.keyFiles.map((file: { path: string; purpose: string }) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className="flex items-start gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<FileCode className="w-4 h-4 text-zinc-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<code className="text-blue-300">{file.path}</code>
|
||||||
|
<p className="text-zinc-500">{file.purpose}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{module.publicApi.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-zinc-400 mb-2">
|
||||||
|
Public API
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{module.publicApi.map((api: string) => (
|
||||||
|
<code
|
||||||
|
key={api}
|
||||||
|
className="px-2 py-1 text-sm bg-blue-500/10 text-blue-300 rounded"
|
||||||
|
>
|
||||||
|
{api}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="patterns" className="scroll-mt-24">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||||
|
<Search className="w-6 h-6 text-blue-400" />
|
||||||
|
Patterns & Conventions
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
<div className="glass rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-white mb-4">Coding Conventions</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{docs.sections.patterns.conventions.map((convention: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||||
|
<span className="text-blue-400 mt-1">•</span>
|
||||||
|
{convention}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-white mb-4">Design Patterns</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{docs.sections.patterns.designPatterns.map((pattern: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start gap-2 text-sm text-zinc-300">
|
||||||
|
<span className="text-blue-400 mt-1">•</span>
|
||||||
|
{pattern}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{docs.sections.patterns.architecturalDecisions.length > 0 && (
|
||||||
|
<div className="mt-6 glass rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-white mb-4">
|
||||||
|
Architectural Decisions
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{docs.sections.patterns.architecturalDecisions.map((decision: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-start gap-3 text-zinc-300">
|
||||||
|
<GitBranch className="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" />
|
||||||
|
{decision}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="getting-started" className="scroll-mt-24">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||||
|
<Rocket className="w-6 h-6 text-blue-400" />
|
||||||
|
Getting Started
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="glass rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-white mb-4">Prerequisites</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{docs.sections.gettingStarted.prerequisites.map((prereq: string, i: number) => (
|
||||||
|
<li key={i} className="flex items-center gap-2 text-sm text-zinc-300">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
||||||
|
{prereq}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass rounded-xl p-6">
|
||||||
|
<h3 className="font-semibold text-white mb-4">Setup Steps</h3>
|
||||||
|
<ol className="space-y-4">
|
||||||
|
{docs.sections.gettingStarted.setupSteps.map((step: string, i: number) => (
|
||||||
|
<li key={i} className="flex gap-4">
|
||||||
|
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-blue-500/20 text-blue-300 text-sm flex items-center justify-center font-medium">
|
||||||
|
{i + 1}
|
||||||
|
</span>
|
||||||
|
<div className="prose prose-invert prose-sm max-w-none">
|
||||||
|
<ReactMarkdown>{step}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass rounded-xl p-6 border-blue-500/20">
|
||||||
|
<h3 className="font-semibold text-white mb-3">First Task</h3>
|
||||||
|
<div className="prose prose-invert prose-sm max-w-none">
|
||||||
|
<ReactMarkdown>{docs.sections.gettingStarted.firstTask}</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{docs.sections.dependencyGraph && (
|
||||||
|
<section className="scroll-mt-24">
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-6 flex items-center gap-3">
|
||||||
|
<GitBranch className="w-6 h-6 text-blue-400" />
|
||||||
|
Dependency Graph
|
||||||
|
</h2>
|
||||||
|
<div className="glass rounded-xl p-6">
|
||||||
|
<MermaidDiagram chart={docs.sections.dependencyGraph} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
apps/web/src/components/footer.tsx
Normal file
58
apps/web/src/components/footer.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Code2, Github, ArrowUpRight } from "lucide-react";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-white/10 bg-black/30">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
|
||||||
|
<Code2 className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-semibold text-white">
|
||||||
|
CodeBoard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<a
|
||||||
|
href="https://company.repi.fun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-1 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Built by Vectry
|
||||||
|
<ArrowUpRight className="w-3 h-3" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-8 border-t border-white/5 text-center">
|
||||||
|
<p className="text-sm text-zinc-500">
|
||||||
|
© {new Date().getFullYear()} CodeBoard. Built by{" "}
|
||||||
|
<a
|
||||||
|
href="https://company.repi.fun"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Vectry
|
||||||
|
</a>
|
||||||
|
. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
apps/web/src/components/mermaid-diagram.tsx
Normal file
82
apps/web/src/components/mermaid-diagram.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
|
interface MermaidDiagramProps {
|
||||||
|
chart: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MermaidDiagram({ chart }: MermaidDiagramProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: "dark",
|
||||||
|
securityLevel: "loose",
|
||||||
|
themeVariables: {
|
||||||
|
darkMode: true,
|
||||||
|
background: "#0a0a0f",
|
||||||
|
primaryColor: "#1e3a5f",
|
||||||
|
primaryTextColor: "#ffffff",
|
||||||
|
primaryBorderColor: "#3b82f6",
|
||||||
|
lineColor: "#6366f1",
|
||||||
|
secondaryColor: "#1f2937",
|
||||||
|
tertiaryColor: "#374151",
|
||||||
|
fontFamily: "ui-monospace, monospace",
|
||||||
|
},
|
||||||
|
flowchart: {
|
||||||
|
useMaxWidth: true,
|
||||||
|
htmlLabels: true,
|
||||||
|
curve: "basis",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isReady || !containerRef.current || !chart) return;
|
||||||
|
|
||||||
|
const renderChart = async () => {
|
||||||
|
try {
|
||||||
|
const id = `mermaid-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
const { svg } = await mermaid.render(id, chart);
|
||||||
|
|
||||||
|
if (containerRef.current) {
|
||||||
|
containerRef.current.innerHTML = svg;
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to render diagram");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderChart();
|
||||||
|
}, [chart, isReady]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||||
|
<p className="text-red-400 text-sm mb-2">Failed to render diagram</p>
|
||||||
|
<pre className="text-xs text-red-300/70 overflow-x-auto">{chart}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="mermaid-diagram overflow-x-auto"
|
||||||
|
style={{ minHeight: "100px" }}
|
||||||
|
>
|
||||||
|
{!isReady && (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="w-6 h-6 border-2 border-blue-500/30 border-t-blue-500 rounded-full animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
apps/web/src/components/navbar.tsx
Normal file
94
apps/web/src/components/navbar.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Code2, Menu, X, Github } from "lucide-react";
|
||||||
|
|
||||||
|
export function Navbar() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{ href: "/#how-it-works", label: "How it Works" },
|
||||||
|
{ href: "/#features", label: "Features" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="fixed top-0 left-0 right-0 z-50">
|
||||||
|
<nav className="glass border-b border-white/5">
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center justify-between h-16">
|
||||||
|
<Link href="/" className="flex items-center gap-2 group">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center group-hover:shadow-lg group-hover:shadow-blue-500/25 transition-shadow">
|
||||||
|
<Code2 className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-semibold text-white">
|
||||||
|
CodeBoard
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="hidden md:flex items-center gap-8">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="md:hidden p-2 text-zinc-400 hover:text-white"
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
) : (
|
||||||
|
<Menu className="w-6 h-6" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div className="md:hidden border-t border-white/5 bg-black/50 backdrop-blur-xl">
|
||||||
|
<div className="px-4 py-4 space-y-3">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link
|
||||||
|
key={link.href}
|
||||||
|
href={link.href}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="block text-sm text-zinc-400 hover:text-white transition-colors py-2"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors py-2"
|
||||||
|
>
|
||||||
|
<Github className="w-4 h-4" />
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -130,17 +130,19 @@ export function ProgressTracker({
|
|||||||
<Circle className="w-4 h-4" />
|
<Circle className="w-4 h-4" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={`flex-1 ${
|
<div className="flex-1">
|
||||||
|
<p
|
||||||
|
className={`font-medium ${
|
||||||
isActive
|
isActive
|
||||||
? "text-white"
|
? "text-white"
|
||||||
: isDone
|
: isDone
|
||||||
? "text-green-400"
|
? "text-zinc-300"
|
||||||
: "text-zinc-500"
|
: "text-zinc-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{step.label}
|
{step.label}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isActive && (
|
{isActive && (
|
||||||
@@ -149,11 +151,12 @@ export function ProgressTracker({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFailed && (
|
{isFailed && (
|
||||||
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
@@ -195,14 +198,13 @@ export function ProgressTracker({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && !isFailed && (
|
{error && !isFailed && (
|
||||||
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-red-400 font-medium">Connection Error</p>
|
<p className="text-red-400 font-medium">Connection Error</p>
|
||||||
<p className="text-red-400/70 text-sm mt-1">
|
<p className="text-red-400/70 text-sm mt-1">{error}</p>
|
||||||
{error}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
106
apps/web/src/components/repo-input.tsx
Normal file
106
apps/web/src/components/repo-input.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Github, Loader2, ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
const GITHUB_URL_REGEX =
|
||||||
|
/^https?:\/\/(www\.)?github\.com\/[\w.-]+\/[\w.-]+\/?$/;
|
||||||
|
|
||||||
|
export function RepoInput() {
|
||||||
|
const [url, setUrl] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isValid = GITHUB_URL_REGEX.test(url);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
setError("Please enter a valid GitHub repository URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/generate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ repoUrl: url.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Failed to start generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
router.push(`/generate?repo=${encodeURIComponent(url)}&id=${data.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
setError(
|
||||||
|
err instanceof Error ? err.message : "An unexpected error occurred"
|
||||||
|
);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="w-full">
|
||||||
|
<div className="relative flex flex-col sm:flex-row gap-3">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
|
||||||
|
<Github className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUrl(e.target.value);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
placeholder="https://github.com/user/repo"
|
||||||
|
className="w-full pl-12 pr-4 py-4 bg-black/40 border border-white/10 rounded-xl text-white placeholder-zinc-500 focus:outline-none focus:border-blue-500/50 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !url}
|
||||||
|
className="flex items-center justify-center gap-2 px-6 py-4 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-medium rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed min-w-[160px]"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
|
<span>Starting...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span>Generate Docs</span>
|
||||||
|
<ArrowRight className="w-4 h-4" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-center gap-2 text-xs text-zinc-500">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${isValid ? "bg-green-500" : "bg-zinc-600"}`} />
|
||||||
|
<span>
|
||||||
|
{isValid
|
||||||
|
? "Valid GitHub URL"
|
||||||
|
: "Enter a public GitHub repository URL"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
apps/worker/src/processor.ts
Normal file
83
apps/worker/src/processor.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import type { Job } from "bullmq";
|
||||||
|
import IORedis from "ioredis";
|
||||||
|
import { cloneRepository } from "./jobs/clone.js";
|
||||||
|
import { parseRepository } from "./jobs/parse.js";
|
||||||
|
import { generateDocs } from "./jobs/generate.js";
|
||||||
|
|
||||||
|
interface GenerateJobData {
|
||||||
|
repoUrl: string;
|
||||||
|
generationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redis = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379");
|
||||||
|
|
||||||
|
async function updateProgress(
|
||||||
|
generationId: string,
|
||||||
|
status: string,
|
||||||
|
progress: number,
|
||||||
|
message?: string
|
||||||
|
) {
|
||||||
|
await redis.publish(
|
||||||
|
`codeboard:progress:${generationId}`,
|
||||||
|
JSON.stringify({ status, progress, message })
|
||||||
|
);
|
||||||
|
await redis.set(
|
||||||
|
`codeboard:status:${generationId}`,
|
||||||
|
JSON.stringify({ status, progress, message }),
|
||||||
|
"EX",
|
||||||
|
3600
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processGenerationJob(
|
||||||
|
job: Job<GenerateJobData>
|
||||||
|
): Promise<unknown> {
|
||||||
|
const { repoUrl, generationId } = job.data;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateProgress(generationId, "CLONING", 10, "Cloning repository...");
|
||||||
|
const cloneResult = await cloneRepository(repoUrl);
|
||||||
|
|
||||||
|
await updateProgress(
|
||||||
|
generationId,
|
||||||
|
"PARSING",
|
||||||
|
30,
|
||||||
|
`Analyzing ${cloneResult.metadata.totalFiles} files...`
|
||||||
|
);
|
||||||
|
const codeStructure = await parseRepository(cloneResult.localPath);
|
||||||
|
|
||||||
|
await updateProgress(
|
||||||
|
generationId,
|
||||||
|
"GENERATING",
|
||||||
|
50,
|
||||||
|
`Generating docs for ${codeStructure.modules.length} modules...`
|
||||||
|
);
|
||||||
|
|
||||||
|
const docs = await generateDocs(codeStructure, (stage, progress) => {
|
||||||
|
const mappedProgress = 50 + Math.floor(progress * 0.4);
|
||||||
|
updateProgress(generationId, "GENERATING", mappedProgress, `${stage}...`);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs.id = generationId;
|
||||||
|
docs.repoUrl = repoUrl;
|
||||||
|
docs.repoName = cloneResult.metadata.name;
|
||||||
|
|
||||||
|
const duration = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
|
||||||
|
await redis.set(
|
||||||
|
`codeboard:result:${generationId}`,
|
||||||
|
JSON.stringify(docs),
|
||||||
|
"EX",
|
||||||
|
86400
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateProgress(generationId, "COMPLETED", 100, "Done!");
|
||||||
|
|
||||||
|
return { generationId, duration, repoName: cloneResult.metadata.name };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : "Unknown error";
|
||||||
|
await updateProgress(generationId, "FAILED", 0, message);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
5920
package-lock.json
generated
Normal file
5920
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ export function buildArchitecturePrompt(
|
|||||||
|
|
||||||
Output format (use exactly these headers):
|
Output format (use exactly these headers):
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
[2-4 paragraphs describing high-level architecture, key design decisions, and how components interact]
|
[2-4 paragraphs describing the high-level architecture, key design decisions, and how components interact]
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
[comma-separated list of technologies detected]
|
[comma-separated list of technologies detected]
|
||||||
@@ -45,7 +45,7 @@ ENTRY POINTS: ${entryPoints}
|
|||||||
DEPENDENCIES (import edges):
|
DEPENDENCIES (import edges):
|
||||||
${structure.dependencies.slice(0, 50).map((d) => ` ${d.source} -> ${d.target}`).join("\n")}
|
${structure.dependencies.slice(0, 50).map((d) => ` ${d.source} -> ${d.target}`).join("\n")}
|
||||||
|
|
||||||
Generate architecture overview with a Mermaid diagram.`,
|
Generate the architecture overview with a Mermaid diagram.`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Output format:
|
|||||||
[numbered list of concrete commands and actions to get the project running locally]
|
[numbered list of concrete commands and actions to get the project running locally]
|
||||||
|
|
||||||
## Your First Task
|
## Your First Task
|
||||||
[suggest a good first contribution — something small but meaningful that touches multiple parts of codebase]`,
|
[suggest a good first contribution — something small but meaningful that touches multiple parts of the codebase]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ export function buildModuleSummaryPrompt(
|
|||||||
|
|
||||||
Output format:
|
Output format:
|
||||||
## Summary
|
## Summary
|
||||||
[1-2 paragraphs explaining what this module does and its role in project]
|
[1-2 paragraphs explaining what this module does and its role in the project]
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
[list each important file with a one-line description]
|
[list each important file with a one-line description]
|
||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
[list main exported functions/classes and what they do]`,
|
[list the main exported functions/classes and what they do]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: "user",
|
||||||
|
|||||||
25
turbo.json
Normal file
25
turbo.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": ["**/.env.*local"],
|
||||||
|
"tasks": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"lint": {},
|
||||||
|
"clean": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:generate": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:push": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user