634 lines
25 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|
|
|