Files
vibn-frontend/app/[workspace]/project/[projectId]/design/page.tsx

458 lines
16 KiB
TypeScript

"use client";
import { use, useState, useEffect } from "react";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Palette,
Plus,
MessageSquare,
History,
Loader2,
CheckCircle2,
Circle,
Clock,
Sparkles,
ExternalLink,
} from "lucide-react";
import { Separator } from "@/components/ui/separator";
import Link from "next/link";
import { toast } from "sonner";
import { TreeView, TreeNode } from "@/components/ui/tree-view";
import { CollapsibleSidebar } from "@/components/ui/collapsible-sidebar";
interface WorkItem {
id: string;
title: string;
path: string;
status: "built" | "in_progress" | "missing";
state?: "draft" | "final";
category: string;
priority: string;
startDate: string | null;
endDate: string | null;
sessionsCount: number;
commitsCount: number;
estimatedCost?: number;
requirements: Array<{
id: number;
text: string;
status: string;
}>;
versionCount?: number;
messageCount?: number;
}
export default function DesignPage({
params,
}: {
params: Promise<{ workspace: string; projectId: string }>;
}) {
const { workspace, projectId } = use(params);
// Helper function to count nodes by status recursively
const countNodesByStatus = (node: TreeNode, status: string): number => {
let count = node.status === status ? 1 : 0;
if (node.children) {
count += node.children.reduce((acc, child) => acc + countNodesByStatus(child, status), 0);
}
return count;
};
const [workItems, setWorkItems] = useState<WorkItem[]>([]);
const [loading, setLoading] = useState(true);
const [filterState, setFilterState] = useState<"all" | "draft" | "final">("all");
const [selectedItem, setSelectedItem] = useState<WorkItem | null>(null);
const [selectedTreeNodeId, setSelectedTreeNodeId] = useState<string | null>(null);
// Sample tree structure - will be populated from AI-generated data
const [treeData] = useState<TreeNode[]>([
{
id: "navigation",
label: "Navigation",
children: [
{
id: "sidebar",
label: "Sidebar",
status: "in_progress",
children: [
{ id: "dashboard", label: "Dashboard", status: "built", metadata: { sessionsCount: 12, commitsCount: 5 } },
{ id: "projects", label: "Projects", status: "in_progress", metadata: { sessionsCount: 8, commitsCount: 3 } },
{ id: "account", label: "Account", status: "missing" },
],
},
{
id: "topnav",
label: "Top Nav",
status: "built",
children: [
{ id: "search", label: "Search", status: "built", metadata: { sessionsCount: 6, commitsCount: 2 } },
{ id: "notifications", label: "Notifications", status: "built", metadata: { sessionsCount: 4, commitsCount: 1 } },
],
},
],
},
{
id: "site",
label: "Site",
children: [
{
id: "home",
label: "Home",
status: "built",
children: [
{ id: "hero", label: "Hero", status: "built", metadata: { sessionsCount: 10, commitsCount: 4 } },
{ id: "features", label: "Features", status: "built", metadata: { sessionsCount: 15, commitsCount: 6 } },
{ id: "testimonials", label: "Testimonials", status: "in_progress", metadata: { sessionsCount: 3, commitsCount: 1 } },
{ id: "cta", label: "CTA", status: "built", metadata: { sessionsCount: 5, commitsCount: 2 } },
],
},
{
id: "blog",
label: "Blog",
status: "missing",
children: [
{ id: "post-list", label: "Post List", status: "missing" },
{ id: "post-page", label: "Post Page", status: "missing" },
],
},
{
id: "pricing",
label: "Pricing",
status: "built",
metadata: { sessionsCount: 7, commitsCount: 3 },
},
],
},
{
id: "onboarding",
label: "Onboarding",
children: [
{ id: "signup", label: "Signup", status: "built", metadata: { sessionsCount: 9, commitsCount: 4 } },
{ id: "magic-link", label: "Magic Link Confirmation", status: "in_progress", metadata: { sessionsCount: 2, commitsCount: 1 } },
{ id: "welcome", label: "Welcome Tour", status: "missing" },
],
},
]);
useEffect(() => {
loadDesignItems();
}, [projectId]);
const loadDesignItems = async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
if (response.ok) {
const data = await response.json();
// Filter for design/user-facing items only
const designItems = data.workItems.filter((item: WorkItem) =>
isTouchpoint(item)
);
setWorkItems(designItems);
}
} catch (error) {
console.error("Error loading design items:", error);
toast.error("Failed to load design items");
} finally {
setLoading(false);
}
};
const isTouchpoint = (item: WorkItem): boolean => {
const path = item.path.toLowerCase();
const title = item.title.toLowerCase();
// Exclude APIs and backend systems
if (path.startsWith('/api/')) return false;
if (title.includes(' api') || title.includes('api ')) return false;
// Exclude pure auth infrastructure (OAuth endpoints)
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return false;
// Include everything else - screens, pages, flows, etc.
return true;
};
const toggleState = async (itemId: string, newState: "draft" | "final") => {
try {
// TODO: Implement API call to update state
setWorkItems(items =>
items.map(item =>
item.id === itemId ? { ...item, state: newState } : item
)
);
toast.success(`Marked as ${newState}`);
} catch (error) {
toast.error("Failed to update state");
}
};
const openInV0 = (item: WorkItem) => {
// TODO: Integrate with v0 API
toast.info("Opening in v0 designer...");
};
const getStatusIcon = (status: string) => {
if (status === "built") return <CheckCircle2 className="h-4 w-4 text-green-600" />;
if (status === "in_progress") return <Clock className="h-4 w-4 text-blue-600" />;
return <Circle className="h-4 w-4 text-gray-400" />;
};
const getStatusLabel = (status: string) => {
if (status === "built") return "Done";
if (status === "in_progress") return "Started";
return "To-do";
};
const filteredItems = workItems.filter(item => {
if (filterState === "all") return true;
return item.state === filterState;
});
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar */}
<CollapsibleSidebar>
<div className="space-y-3">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Design Assets
</h3>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
{workItems.length}
</Badge>
</div>
{/* Tree View */}
<TreeView
data={treeData}
selectedId={selectedTreeNodeId}
onSelect={(node) => {
setSelectedTreeNodeId(node.id);
toast.info(`Selected: ${node.label}`);
}}
/>
{/* Quick Stats at Bottom */}
<div className="pt-3 mt-3 border-t space-y-1.5 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Built</span>
<span className="font-medium text-green-600">
{treeData.reduce((acc, node) => acc + countNodesByStatus(node, "built"), 0)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">In Progress</span>
<span className="font-medium text-blue-600">
{treeData.reduce((acc, node) => acc + countNodesByStatus(node, "in_progress"), 0)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">To Build</span>
<span className="font-medium text-gray-600">
{treeData.reduce((acc, node) => acc + countNodesByStatus(node, "missing"), 0)}
</span>
</div>
</div>
</div>
</CollapsibleSidebar>
{/* Main Content */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="border-b bg-background p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Palette className="h-6 w-6" />
<div>
<h1 className="text-xl font-bold">Design</h1>
<p className="text-sm text-muted-foreground">
User-facing screens, features, and design assets
</p>
</div>
</div>
<Button className="gap-2">
<Plus className="h-4 w-4" />
New Design
</Button>
</div>
{/* Filters */}
<div className="flex items-center gap-2 mt-4">
<Button
variant={filterState === "all" ? "secondary" : "ghost"}
size="sm"
onClick={() => setFilterState("all")}
>
All
</Button>
<Button
variant={filterState === "draft" ? "secondary" : "ghost"}
size="sm"
onClick={() => setFilterState("draft")}
>
Draft
</Button>
<Button
variant={filterState === "final" ? "secondary" : "ghost"}
size="sm"
onClick={() => setFilterState("final")}
>
Final
</Button>
<Separator orientation="vertical" className="h-6 mx-2" />
<Badge variant="secondary">
{filteredItems.length} items
</Badge>
</div>
</div>
{/* Work Items List */}
<div className="flex-1 overflow-auto p-4">
{loading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : filteredItems.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-center">
<Palette className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">No design items yet</h3>
<p className="text-sm text-muted-foreground mb-4">
Design items are user-facing elements like screens and features
</p>
<Button>
<Plus className="h-4 w-4 mr-2" />
Create First Design
</Button>
</div>
) : (
<div className="space-y-3">
{filteredItems.map((item) => (
<Card key={item.id} className="p-4 hover:bg-accent/30 transition-colors">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3 flex-1">
{getStatusIcon(item.status)}
<div className="flex-1 space-y-2">
{/* Title and Status */}
<div className="flex items-center gap-2">
<h3 className="font-semibold">{item.title}</h3>
<Badge variant="outline" className="text-xs">
{getStatusLabel(item.status)}
</Badge>
{item.state && (
<Badge
variant={item.state === "final" ? "default" : "secondary"}
className="text-xs"
>
{item.state === "final" ? "Final" : "Draft"}
</Badge>
)}
</div>
{/* Path */}
<p className="text-sm text-muted-foreground font-mono">
{item.path}
</p>
{/* Stats */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span>{item.sessionsCount} sessions</span>
<span></span>
<span>{item.commitsCount} commits</span>
{item.estimatedCost && (
<>
<span></span>
<span>${item.estimatedCost.toFixed(2)}</span>
</>
)}
{item.versionCount && (
<>
<span></span>
<span>{item.versionCount} versions</span>
</>
)}
{item.messageCount && (
<>
<span></span>
<span>{item.messageCount} messages</span>
</>
)}
</div>
{/* Requirements Preview */}
{item.requirements.length > 0 && (
<div className="mt-2">
<p className="text-xs text-muted-foreground mb-1">
{item.requirements.filter(r => r.status === "built").length} of{" "}
{item.requirements.length} requirements complete
</p>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="gap-2"
onClick={() => openInV0(item)}
>
<Sparkles className="h-4 w-4" />
Design
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.info("Version history coming soon")}
>
<History className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => toast.info("Messages coming soon")}
>
<MessageSquare className="h-4 w-4" />
</Button>
{/* State Toggle */}
{item.state !== "final" && (
<Button
variant="default"
size="sm"
onClick={() => toggleState(item.id, "final")}
>
Mark Final
</Button>
)}
{item.state === "final" && (
<Button
variant="outline"
size="sm"
onClick={() => toggleState(item.id, "draft")}
>
Back to Draft
</Button>
)}
</div>
</div>
</Card>
))}
</div>
)}
</div>
</div>
{/* End Main Content */}
</div>
);
}