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:
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>
|
||||
);
|
||||
}
|
||||
213
apps/web/src/components/progress-tracker.tsx
Normal file
213
apps/web/src/components/progress-tracker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user