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

634 lines
25 KiB
TypeScript

"use client";
import { use, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { Eye, MessageSquare, Copy, Share2, Sparkles, History, Loader2, Send, MousePointer2 } from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
// Mock data for page variations
const mockPageData: Record<string, any> = {
"landing-hero": {
name: "Landing Page Hero",
emoji: "✨",
style: "modern",
prompt: "Create a modern landing page hero section with gradient background",
v0Url: "https://v0.dev/chat/abc123",
variations: [
{
id: 1,
name: "Version 1 - Blue Gradient",
thumbnail: "https://placehold.co/800x600/1e40af/ffffff?text=Hero+V1",
createdAt: "2025-11-11",
views: 45,
comments: 3,
},
{
id: 2,
name: "Version 2 - Purple Gradient",
thumbnail: "https://placehold.co/800x600/7c3aed/ffffff?text=Hero+V2",
createdAt: "2025-11-10",
views: 32,
comments: 2,
},
{
id: 3,
name: "Version 3 - Minimal",
thumbnail: "https://placehold.co/800x600/6b7280/ffffff?text=Hero+V3",
createdAt: "2025-11-09",
views: 28,
comments: 1,
},
],
},
"dashboard": {
name: "Dashboard Layout",
emoji: "📊",
style: "minimal",
prompt: "Design a clean dashboard with sidebar, metrics cards, and charts",
v0Url: "https://v0.dev/chat/def456",
variations: [
{
id: 1,
name: "Version 1 - Default",
thumbnail: "https://placehold.co/800x600/7c3aed/ffffff?text=Dashboard+V1",
createdAt: "2025-11-10",
views: 78,
comments: 8,
},
],
},
"pricing": {
name: "Pricing Cards",
emoji: "💳",
style: "colorful",
prompt: "Three-tier pricing cards with features, hover effects, and CTA buttons",
v0Url: "https://v0.dev/chat/ghi789",
variations: [
{
id: 1,
name: "Version 1 - Standard",
thumbnail: "https://placehold.co/800x600/059669/ffffff?text=Pricing+V1",
createdAt: "2025-11-09",
views: 102,
comments: 12,
},
{
id: 2,
name: "Version 2 - Compact",
thumbnail: "https://placehold.co/800x600/0891b2/ffffff?text=Pricing+V2",
createdAt: "2025-11-08",
views: 67,
comments: 5,
},
],
},
"user-profile": {
name: "User Profile",
emoji: "👤",
style: "modern",
prompt: "User profile page with avatar, bio, stats, and activity feed",
v0Url: "https://v0.dev/chat/jkl012",
variations: [
{
id: 1,
name: "Version 1 - Default",
thumbnail: "https://placehold.co/800x600/dc2626/ffffff?text=Profile+V1",
createdAt: "2025-11-08",
views: 56,
comments: 5,
},
],
},
};
export default function DesignPageView({
params,
}: {
params: Promise<{ projectId: string; pageSlug: string }>;
}) {
const { projectId, pageSlug } = use(params);
const pageData = mockPageData[pageSlug] || mockPageData["landing-hero"];
const [editPrompt, setEditPrompt] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const [currentVersion, setCurrentVersion] = useState(pageData.variations[0]);
const [versionsModalOpen, setVersionsModalOpen] = useState(false);
const [commentsModalOpen, setCommentsModalOpen] = useState(false);
const [chatMessage, setChatMessage] = useState("");
const [pageName, setPageName] = useState(pageData.name);
const [isEditingName, setIsEditingName] = useState(false);
const [designModeActive, setDesignModeActive] = useState(false);
const [selectedElement, setSelectedElement] = useState<string | null>(null);
const handleIterate = async () => {
if (!editPrompt.trim()) {
toast.error("Please enter a prompt to iterate");
return;
}
setIsGenerating(true);
try {
// Call v0 API to generate update
const response = await fetch('/api/v0/iterate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chatId: pageData.v0Url.split('/').pop(),
message: editPrompt,
projectId,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to iterate');
}
toast.success("Design updated!", {
description: "Your changes have been generated",
});
// Refresh or update the current version
setEditPrompt("");
} catch (error) {
console.error('Error iterating:', error);
toast.error(error instanceof Error ? error.message : "Failed to iterate design");
} finally {
setIsGenerating(false);
}
};
const handlePushToCursor = () => {
toast.success("Code will be pushed to Cursor", {
description: "This feature will send the component code to your IDE",
});
// TODO: Implement actual push to Cursor IDE
};
return (
<>
<div className="flex h-full flex-col overflow-hidden">
{/* Toolbar */}
<div className="border-b bg-card/50 px-6 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
{isEditingName ? (
<input
type="text"
value={pageName}
onChange={(e) => setPageName(e.target.value)}
onBlur={() => {
setIsEditingName(false);
toast.success("Page name updated");
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setIsEditingName(false);
toast.success("Page name updated");
}
}}
className="text-lg font-semibold bg-transparent border-b border-primary outline-none px-1 min-w-[200px]"
autoFocus
/>
) : (
<h1
className="text-lg font-semibold cursor-pointer hover:text-primary transition-colors"
onClick={() => setIsEditingName(true)}
>
{pageName}
</h1>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setVersionsModalOpen(true)}
>
<History className="h-4 w-4 mr-2" />
Versions
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCommentsModalOpen(true)}
>
<MessageSquare className="h-4 w-4 mr-2" />
Comments
</Button>
<Button
variant="outline"
size="sm"
onClick={handlePushToCursor}
>
<Send className="h-4 w-4 mr-2" />
Push to Cursor
</Button>
<Button variant="outline" size="sm">
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
</div>
</div>
{/* Live Preview */}
<div className="flex-1 overflow-auto bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800 relative">
<div className="w-full h-full p-8">
{/* Sample SaaS Dashboard Component */}
<div className="mx-auto max-w-7xl space-y-6">
{/* Page Header */}
<div
data-element="page-header"
className={cn(
"flex items-center justify-between transition-all p-2 rounded-lg",
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
selectedElement === "page-header" && "ring-2 ring-primary ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("page-header");
}
}}
>
<div>
<h1
data-element="page-title"
className={cn(
"text-3xl font-bold transition-all rounded px-1",
designModeActive && "hover:ring-2 hover:ring-primary/50 hover:ring-inset",
selectedElement === "page-title" && "ring-2 ring-primary/50 ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("page-title");
}
}}
>
Dashboard Overview
</h1>
<p className="text-muted-foreground mt-1">Welcome back! Here's what's happening today.</p>
</div>
<Button
data-element="primary-action-button"
className={cn(
"transition-all",
designModeActive && "hover:ring-2 hover:ring-yellow-400 hover:ring-inset",
selectedElement === "primary-action-button" && "ring-2 ring-yellow-400 ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("primary-action-button");
}
}}
>
Create New Project
</Button>
</div>
{/* Stats Grid */}
<div
data-element="stats-grid"
className={cn(
"grid md:grid-cols-4 gap-4 transition-all rounded-xl",
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
selectedElement === "stats-grid" && "ring-2 ring-primary ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("stats-grid");
}
}}
>
{[
{ label: "Total Users", value: "2,847", change: "+12.3%", trend: "up" },
{ label: "Revenue", value: "$45,231", change: "+8.1%", trend: "up" },
{ label: "Active Projects", value: "127", change: "-2.4%", trend: "down" },
{ label: "Conversion Rate", value: "3.24%", change: "+0.8%", trend: "up" },
].map((stat, i) => (
<Card
key={i}
data-element={`stat-card-${i}`}
className={cn(
"transition-all",
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
selectedElement === `stat-card-${i}` && "ring-2 ring-primary ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement(`stat-card-${i}`);
}
}}
>
<CardHeader className="pb-2">
<CardDescription className="text-xs">{stat.label}</CardDescription>
<CardTitle className="text-2xl">{stat.value}</CardTitle>
<span className={cn(
"text-xs font-medium",
stat.trend === "up" ? "text-green-600" : "text-red-600"
)}>
{stat.change}
</span>
</CardHeader>
</Card>
))}
</div>
{/* Data Table */}
<Card
data-element="data-table"
className={cn(
"transition-all",
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset",
selectedElement === "data-table" && "ring-2 ring-primary ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("data-table");
}
}}
>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Recent Projects</CardTitle>
<CardDescription>Your team's latest work</CardDescription>
</div>
<Button
variant="outline"
size="sm"
data-element="table-action-button"
className={cn(
"transition-all",
designModeActive && "hover:ring-2 hover:ring-yellow-400 hover:ring-inset",
selectedElement === "table-action-button" && "ring-2 ring-yellow-400 ring-inset"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement("table-action-button");
}
}}
>
View All
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[
{ name: "Mobile App Redesign", status: "In Progress", team: "Design Team", updated: "2 hours ago" },
{ name: "API Documentation", status: "Review", team: "Engineering", updated: "5 hours ago" },
{ name: "Marketing Website", status: "Completed", team: "Marketing", updated: "1 day ago" },
{ name: "User Dashboard v2", status: "Planning", team: "Product", updated: "3 days ago" },
].map((project, i) => (
<div
key={i}
data-element={`table-row-${i}`}
className={cn(
"flex items-center justify-between p-3 rounded-lg border transition-all",
designModeActive && "cursor-pointer hover:ring-2 hover:ring-primary hover:ring-inset hover:bg-accent",
selectedElement === `table-row-${i}` && "ring-2 ring-primary ring-inset bg-accent"
)}
onClick={(e) => {
if (designModeActive) {
e.stopPropagation();
setSelectedElement(`table-row-${i}`);
}
}}
>
<div className="flex-1">
<p className="font-medium">{project.name}</p>
<p className="text-sm text-muted-foreground">{project.team}</p>
</div>
<div className="flex items-center gap-4">
<span className={cn(
"text-xs font-medium px-2 py-1 rounded-full",
project.status === "Completed" && "bg-green-100 text-green-700",
project.status === "In Progress" && "bg-blue-100 text-blue-700",
project.status === "Review" && "bg-yellow-100 text-yellow-700",
project.status === "Planning" && "bg-gray-100 text-gray-700"
)}>
{project.status}
</span>
<span className="text-sm text-muted-foreground w-24 text-right">{project.updated}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
{/* Floating Chat Interface - v0 Style */}
<div
className="absolute bottom-6 left-1/2 -translate-x-1/2 w-full max-w-3xl px-6"
>
<div className="bg-background/95 backdrop-blur-lg border border-border rounded-2xl shadow-2xl overflow-hidden">
{/* Input Area */}
<div className="p-3 relative">
<Textarea
placeholder="e.g., 'Make the hero section more vibrant', 'Add a call-to-action button', 'Change the color scheme to dark mode'"
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
className="min-h-[60px] resize-none border-0 bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 text-sm px-1"
disabled={isGenerating}
rows={2}
/>
</div>
{/* Action Bar */}
<div className="px-4 pb-3 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Button
variant={designModeActive ? "default" : "ghost"}
size="sm"
onClick={() => {
setDesignModeActive(!designModeActive);
setSelectedElement(null);
}}
>
<MousePointer2 className="h-4 w-4 mr-2" />
Design Mode
</Button>
{selectedElement && (
<div className="flex items-center gap-2 px-2 py-1 bg-primary/10 text-primary rounded text-xs">
<MousePointer2 className="h-3 w-3" />
<span className="font-medium">{selectedElement.replace(/-/g, ' ')}</span>
</div>
)}
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
disabled={isGenerating}
onClick={() => {
toast.info("Creating variation...");
}}
>
<Copy className="h-4 w-4 mr-1" />
Variation
</Button>
<Button
size="sm"
onClick={() => {
const contextualPrompt = selectedElement
? `[Targeting: ${selectedElement.replace(/-/g, ' ')}] ${chatMessage}`
: chatMessage;
setEditPrompt(contextualPrompt);
handleIterate();
}}
disabled={isGenerating || !chatMessage.trim()}
className="gap-2"
>
{isGenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Generating
</>
) : (
<>
<Sparkles className="h-4 w-4" />
{selectedElement ? 'Modify Selected' : 'Generate'}
</>
)}
</Button>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Versions Modal */}
<Dialog open={versionsModalOpen} onOpenChange={setVersionsModalOpen}>
<DialogContent className="max-w-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Version History</DialogTitle>
<DialogDescription>
View and switch between different versions of this design
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[60vh] pr-4">
<div className="space-y-3">
{pageData.variations.map((variation: any) => (
<button
key={variation.id}
onClick={() => {
setCurrentVersion(variation);
setVersionsModalOpen(false);
toast.success(`Switched to ${variation.name}`);
}}
className={`w-full text-left rounded-lg border p-4 transition-colors hover:bg-accent ${
currentVersion.id === variation.id ? 'border-primary bg-accent' : ''
}`}
>
<div className="flex items-start gap-4">
<img
src={variation.thumbnail}
alt={variation.name}
className="w-32 h-20 rounded object-cover"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-base">{variation.name}</h4>
<p className="text-sm text-muted-foreground mt-1">
{variation.createdAt}
</p>
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Eye className="h-4 w-4" />
{variation.views} views
</span>
<span className="flex items-center gap-1">
<MessageSquare className="h-4 w-4" />
{variation.comments} comments
</span>
</div>
</div>
</div>
</button>
))}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
{/* Comments Modal */}
<Dialog open={commentsModalOpen} onOpenChange={setCommentsModalOpen}>
<DialogContent className="max-w-2xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Comments & Feedback</DialogTitle>
<DialogDescription>
Discuss this design with your team
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[50vh] pr-4">
<div className="space-y-4">
{/* Mock comments */}
<div className="space-y-3">
<div className="rounded-lg border p-4 space-y-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-sm font-medium">
JD
</div>
<div className="flex-1">
<span className="text-sm font-medium">Jane Doe</span>
<span className="text-xs text-muted-foreground ml-2">2h ago</span>
</div>
</div>
<p className="text-sm text-muted-foreground">
Love the gradient! Could we try a darker variant?
</p>
</div>
<div className="rounded-lg border p-4 space-y-2">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-green-500/10 flex items-center justify-center text-sm font-medium">
MS
</div>
<div className="flex-1">
<span className="text-sm font-medium">Mike Smith</span>
<span className="text-xs text-muted-foreground ml-2">5h ago</span>
</div>
</div>
<p className="text-sm text-muted-foreground">
The layout looks perfect. Spacing is on point 👍
</p>
</div>
</div>
</div>
</ScrollArea>
{/* Add comment */}
<div className="pt-4 border-t space-y-3">
<Textarea
placeholder="Add a comment..."
className="min-h-[100px] resize-none"
/>
<Button className="w-full">
<MessageSquare className="h-4 w-4 mr-2" />
Post Comment
</Button>
</div>
</DialogContent>
</Dialog>
</>
);
}