feat: initial CodeBoard monorepo scaffold

Turborepo monorepo with npm workspaces:
- apps/web: Next.js 14 frontend with Tailwind v4, SSE progress, doc viewer
- apps/worker: BullMQ job processor (clone → parse → LLM generate)
- packages/shared: TypeScript types
- packages/parser: Babel-based AST parser (JS/TS) + regex (Python)
- packages/llm: OpenAI/Anthropic provider abstraction + prompt pipeline
- packages/diagrams: Mermaid architecture & dependency graph generators
- packages/database: Prisma schema (PostgreSQL)
- Docker multi-stage build (web + worker targets)

All packages compile successfully with tsc and next build.
This commit is contained in:
Vectry
2026-02-09 15:22:50 +00:00
parent efdc282da5
commit 79dad6124f
72 changed files with 10132 additions and 136 deletions

5
apps/web/next-env.d.ts vendored Normal file
View 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
View File

@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const config = {
transpilePackages: ["@codeboard/shared"],
output: "standalone",
};
export default config;

33
apps/web/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "@codeboard/web",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"db:generate": "prisma generate --schema=../../packages/database/prisma/schema.prisma",
"db:push": "prisma db push --schema=../../packages/database/prisma/schema.prisma"
},
"dependencies": {
"@codeboard/shared": "*",
"bullmq": "^5.34.0",
"ioredis": "^5.4.0",
"lucide-react": "^0.563.0",
"mermaid": "^11.4.0",
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-markdown": "^9.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.0.0",
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"postcss": "^8.5.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7"
}
}

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

0
apps/web/public/.gitkeep Normal file
View File

View File

@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import { getRedis } from "@/lib/redis";
export async function GET(
_request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const redis = getRedis();
const result = await redis.get(`codeboard:result:${id}`);
if (!result) {
return NextResponse.json(
{ error: "Documentation not found" },
{ status: 404 }
);
}
return NextResponse.json(JSON.parse(result));
}

View 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 }
);
}

View 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",
},
});
}

View File

@@ -0,0 +1,64 @@
import { DocViewer } from "@/components/doc-viewer";
import type { GeneratedDocs } from "@codeboard/shared";
import { notFound } from "next/navigation";
import { Github, ArrowLeft } from "lucide-react";
import Link from "next/link";
async function fetchDocs(id: string): Promise<GeneratedDocs | null> {
try {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
const response = await fetch(`${baseUrl}/api/docs/${id}`, {
cache: "no-store",
});
if (!response.ok) {
return null;
}
return response.json();
} catch {
return null;
}
}
export default async function DocsPage({
params,
}: {
params: { id: string };
}) {
const docs = await fetchDocs(params.id);
if (!docs) {
notFound();
}
return (
<div className="min-h-screen">
<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-4">
<div className="flex items-center justify-between">
<Link
href="/"
className="flex items-center gap-2 text-sm text-zinc-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-4 h-4" />
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>
<DocViewer docs={docs} />
</div>
);
}

View File

@@ -0,0 +1,82 @@
import { Suspense } from "react";
import { ProgressTracker } from "@/components/progress-tracker";
import { Github, Loader2 } from "lucide-react";
function GeneratePageContent({
searchParams,
}: {
searchParams: { repo?: string; id?: string };
}) {
const repoUrl = searchParams.repo || "";
const generationId = searchParams.id || "";
const repoName = repoUrl
? repoUrl.replace("https://github.com/", "").replace(/\/$/, "")
: "";
return (
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="text-center mb-12">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl glass mb-6">
<Github className="w-8 h-8 text-zinc-400" />
</div>
<h1 className="text-2xl sm:text-3xl font-bold text-white mb-3">
Analyzing Repository
</h1>
<p className="text-zinc-400 font-mono text-sm break-all max-w-md mx-auto">
{repoName || repoUrl || "Unknown repository"}
</p>
</div>
{generationId ? (
<ProgressTracker generationId={generationId} repoUrl={repoUrl} />
) : (
<div className="text-center p-8 rounded-2xl glass border-red-500/30">
<p className="text-red-400">
Missing generation ID. Please try again.
</p>
</div>
)}
</div>
);
}
function GeneratePageSkeleton() {
return (
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
<div className="text-center mb-12">
<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" />
</div>
<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>
<div className="space-y-4">
{[1, 2, 3, 4, 5].map((i) => (
<div
key={i}
className="flex items-center gap-4 p-4 rounded-xl bg-zinc-900/50"
>
<div className="w-8 h-8 rounded-full bg-zinc-800 animate-pulse" />
<div className="flex-1 h-4 bg-zinc-800 rounded animate-pulse" />
</div>
))}
</div>
</div>
);
}
export default function GeneratePage({
searchParams,
}: {
searchParams: { repo?: string; id?: string };
}) {
return (
<Suspense fallback={<GeneratePageSkeleton />}>
<GeneratePageContent searchParams={searchParams} />
</Suspense>
);
}

View File

@@ -0,0 +1,365 @@
@import "tailwindcss";
:root {
--background: #0a0a0f;
--surface: rgba(255, 255, 255, 0.03);
--surface-hover: rgba(255, 255, 255, 0.06);
--border: rgba(255, 255, 255, 0.08);
--border-strong: rgba(255, 255, 255, 0.15);
--text-primary: #ffffff;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--accent-blue: #3b82f6;
--accent-indigo: #6366f1;
--accent-purple: #9333ea;
--accent-cyan: #06b6d4;
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #9333ea 100%);
--gradient-subtle: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(147, 51, 234, 0.1) 100%);
--shadow-glow: 0 0 40px rgba(59, 130, 246, 0.3);
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--background);
color: var(--text-primary);
font-feature-settings: "rlig" 1, "calt" 1;
}
::selection {
background: rgba(59, 130, 246, 0.3);
color: white;
}
.glass {
background: var(--surface);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
}
.glass-strong {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid var(--border-strong);
}
.gradient-text {
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gradient-border {
position: relative;
}
.gradient-border::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: var(--gradient-primary);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
.glow {
box-shadow: var(--shadow-glow);
}
.glow-subtle {
box-shadow: 0 0 60px rgba(59, 130, 246, 0.15);
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-glow {
0%, 100% {
box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
}
50% {
box-shadow: 0 0 40px rgba(59, 130, 246, 0.6), 0 0 60px rgba(147, 51, 234, 0.3);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes spin-slow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-fade-in {
animation: fade-in 0.6s ease-out forwards;
}
.animate-slide-up {
animation: slide-up 0.6s ease-out forwards;
}
.animate-slide-down {
animation: slide-down 0.4s ease-out forwards;
}
.animate-scale-in {
animation: scale-in 0.4s ease-out forwards;
}
.animate-pulse-glow {
animation: pulse-glow 2s ease-in-out infinite;
}
.animate-shimmer {
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.animate-float {
animation: float 6s ease-in-out infinite;
}
.animate-spin-slow {
animation: spin-slow 20s linear infinite;
}
.stagger-1 { animation-delay: 0.1s; }
.stagger-2 { animation-delay: 0.2s; }
.stagger-3 { animation-delay: 0.3s; }
.stagger-4 { animation-delay: 0.4s; }
.stagger-5 { animation-delay: 0.5s; }
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.bg-grid {
background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 60px 60px;
}
.bg-gradient-radial {
background: radial-gradient(ellipse at top, rgba(59, 130, 246, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom, rgba(147, 51, 234, 0.1) 0%, transparent 50%);
}
.noise {
position: relative;
}
.noise::after {
content: "";
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
opacity: 0.03;
pointer-events: none;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
font-weight: 500;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 8px 30px rgba(59, 130, 246, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
background: var(--surface);
color: var(--text-primary);
font-weight: 500;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.btn-secondary:hover {
background: var(--surface-hover);
border-color: var(--border-strong);
}
.input-field {
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 0.75rem 1rem;
color: var(--text-primary);
transition: all 0.2s ease;
width: 100%;
}
.input-field:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.input-field::placeholder {
color: var(--text-muted);
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 1rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.card:hover {
background: var(--surface-hover);
border-color: var(--border-strong);
transform: translateY(-2px);
}
.icon-box {
width: 3rem;
height: 3rem;
border-radius: 0.75rem;
background: var(--gradient-subtle);
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-blue);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary);
}
.code-block {
background: rgba(0, 0, 0, 0.5);
border: 1px solid var(--border);
border-radius: 0.75rem;
padding: 1rem;
font-family: var(--font-mono), ui-monospace, monospace;
font-size: 0.875rem;
overflow-x: auto;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent);
}

View File

@@ -0,0 +1,58 @@
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
const inter = Inter({
subsets: ["latin"],
variable: "--font-sans",
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-mono",
display: "swap",
});
export const metadata: Metadata = {
title: "CodeBoard — Understand any codebase in 5 minutes",
description:
"Paste a GitHub URL and get interactive onboarding documentation with architecture diagrams, module breakdowns, and getting started guides. Built by Vectry AI consultancy.",
keywords: ["code analysis", "documentation", "github", "codebase", "AI", "developer tools"],
authors: [{ name: "Vectry" }],
openGraph: {
title: "CodeBoard — Understand any codebase in 5 minutes",
description:
"Paste a GitHub URL and get interactive onboarding documentation with architecture diagrams, module breakdowns, and getting started guides.",
type: "website",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body
className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased bg-[#0a0a0f] text-white min-h-screen`}
>
<div className="relative min-h-screen flex flex-col">
<div className="fixed inset-0 bg-gradient-radial pointer-events-none" />
<div className="fixed inset-0 bg-grid pointer-events-none opacity-50" />
<Navbar />
<main className="flex-1 relative">
{children}
</main>
<Footer />
</div>
</body>
</html>
);
}

280
apps/web/src/app/page.tsx Normal file
View 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&apos;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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,213 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import type { GenerationStatus } from "@codeboard/shared";
import {
Loader2,
CheckCircle2,
Circle,
AlertCircle,
FileText,
RefreshCw,
} from "lucide-react";
interface ProgressData {
status: GenerationStatus;
progress: number;
message: string;
}
interface ProgressTrackerProps {
generationId: string;
repoUrl: string;
}
const STEPS: { status: GenerationStatus; label: string }[] = [
{ status: "QUEUED", label: "Queued" },
{ status: "CLONING", label: "Cloning Repository" },
{ status: "PARSING", label: "Analyzing Code" },
{ status: "GENERATING", label: "Generating Docs" },
{ status: "RENDERING", label: "Finalizing" },
];
export function ProgressTracker({
generationId,
repoUrl,
}: ProgressTrackerProps) {
const [data, setData] = useState<ProgressData>({
status: "QUEUED",
progress: 0,
message: "Waiting in queue...",
});
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const eventSource = new EventSource(`/api/status/${generationId}`);
eventSource.addEventListener("progress", (event) => {
try {
const parsed = JSON.parse(event.data);
setData(parsed);
if (parsed.status === "COMPLETED" || parsed.status === "FAILED") {
eventSource.close();
}
} catch {
setError("Failed to parse progress data");
}
});
eventSource.addEventListener("timeout", () => {
setError("Connection timed out. Please refresh the page.");
eventSource.close();
});
eventSource.onerror = () => {
setError("Connection error. Please refresh the page.");
eventSource.close();
};
return () => {
eventSource.close();
};
}, [generationId]);
const getStepIndex = (status: GenerationStatus) => {
if (status === "COMPLETED") return STEPS.length;
if (status === "FAILED") return -1;
return STEPS.findIndex((s) => s.status === status);
};
const currentStepIndex = getStepIndex(data.status);
const isCompleted = data.status === "COMPLETED";
const isFailed = data.status === "FAILED";
return (
<div className="space-y-6">
<div className="relative h-2 bg-zinc-800 rounded-full overflow-hidden">
<div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 via-indigo-500 to-purple-500 transition-all duration-500 ease-out"
style={{ width: `${data.progress}%` }}
/>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-zinc-400">{data.message}</span>
<span className="text-zinc-500 font-mono">{data.progress}%</span>
</div>
<div className="space-y-3">
{STEPS.map((step, index) => {
const isActive = index === currentStepIndex;
const isDone = index < currentStepIndex || isCompleted;
return (
<div
key={step.status}
className={`flex items-center gap-4 p-4 rounded-xl border transition-all duration-300 ${
isActive
? "bg-blue-500/10 border-blue-500/30"
: isDone
? "bg-zinc-900/50 border-zinc-800"
: "bg-transparent border-zinc-800/50"
}`}
>
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
isActive
? "bg-blue-500/20 text-blue-400"
: isDone
? "bg-green-500/20 text-green-400"
: "bg-zinc-800 text-zinc-500"
}`}
>
{isActive ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : isDone ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<Circle className="w-4 h-4" />
)}
</div>
<div className="flex-1">
<p
className={`font-medium ${
isActive
? "text-white"
: isDone
? "text-zinc-300"
: "text-zinc-500"
}`}
>
{step.label}
</p>
</div>
{isActive && (
<div className="flex-shrink-0">
<div className="w-2 h-2 rounded-full bg-blue-400 animate-pulse" />
</div>
)}
</div>
);
})}
</div>
{isFailed && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-400 font-medium">Generation Failed</p>
<p className="text-red-400/70 text-sm mt-1">
Something went wrong. Please try again.
</p>
</div>
</div>
<button
onClick={() => window.location.reload()}
className="mt-4 flex items-center gap-2 px-4 py-2 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors text-sm"
>
<RefreshCw className="w-4 h-4" />
Try Again
</button>
</div>
)}
{isCompleted && (
<div className="p-6 rounded-xl bg-green-500/10 border border-green-500/20 text-center">
<div className="w-12 h-12 rounded-full bg-green-500/20 flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-6 h-6 text-green-400" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">
Documentation Ready!
</h3>
<p className="text-zinc-400 text-sm mb-6">
Your interactive documentation has been generated successfully.
</p>
<Link
href={`/docs/${generationId}`}
className="inline-flex items-center gap-2 px-6 py-3 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"
>
<FileText className="w-4 h-4" />
View Documentation
</Link>
</div>
)}
{error && !isFailed && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-red-400 font-medium">Connection Error</p>
<p className="text-red-400/70 text-sm mt-1">{error}</p>
</div>
</div>
</div>
)}
</div>
);
}

View 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>
);
}

13
apps/web/src/lib/queue.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Queue } from "bullmq";
import { getRedis } from "./redis";
let queue: Queue | null = null;
export function getQueue(): Queue {
if (!queue) {
queue = new Queue("codeboard:generate", {
connection: getRedis(),
});
}
return queue;
}

12
apps/web/src/lib/redis.ts Normal file
View File

@@ -0,0 +1,12 @@
import IORedis from "ioredis";
let redis: IORedis | null = null;
export function getRedis(): IORedis {
if (!redis) {
redis = new IORedis(process.env.REDIS_URL ?? "redis://localhost:6379", {
maxRetriesPerRequest: null,
});
}
return redis;
}

23
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}