VIBN Frontend for Coolify deployment
This commit is contained in:
457
app/[workspace]/project/[projectId]/design/page.tsx
Normal file
457
app/[workspace]/project/[projectId]/design/page.tsx
Normal file
@@ -0,0 +1,457 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user