VIBN Frontend for Coolify deployment
This commit is contained in:
@@ -0,0 +1,633 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
558
app/[workspace]/project/[projectId]/design-old/page.tsx
Normal file
558
app/[workspace]/project/[projectId]/design-old/page.tsx
Normal file
@@ -0,0 +1,558 @@
|
||||
"use client";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Sparkles, ChevronRight, ChevronDown, Folder, FileText, Palette, LayoutGrid, Workflow, Github, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { toast } from "sonner";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard as TemplateCard,
|
||||
} from "@/components/layout/page-template";
|
||||
|
||||
// Mock tree structure - Core Product screens
|
||||
const coreProductTree = [
|
||||
{
|
||||
id: "dashboard",
|
||||
name: "Dashboard",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "overview", name: "Overview", type: "page", route: "/dashboard", variations: 2 },
|
||||
{ id: "analytics", name: "Analytics", type: "page", route: "/dashboard/analytics", variations: 1 },
|
||||
{ id: "projects", name: "Projects", type: "page", route: "/dashboard/projects", variations: 2 },
|
||||
{ id: "activity", name: "Activity", type: "page", route: "/dashboard/activity", variations: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "profile",
|
||||
name: "Profile & Settings",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "user-profile", name: "User Profile", type: "page", route: "/profile", variations: 2 },
|
||||
{ id: "edit-profile", name: "Edit Profile", type: "page", route: "/profile/edit", variations: 1 },
|
||||
{ id: "account", name: "Account Settings", type: "page", route: "/settings/account", variations: 1 },
|
||||
{ id: "billing", name: "Billing", type: "page", route: "/settings/billing", variations: 2 },
|
||||
{ id: "notifications", name: "Notifications", type: "page", route: "/settings/notifications", variations: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// AI-suggested screens for Core Product
|
||||
const suggestedCoreScreens = [
|
||||
{
|
||||
id: "team-management",
|
||||
name: "Team Management",
|
||||
reason: "Collaborate with team members and manage permissions",
|
||||
version: "V1",
|
||||
},
|
||||
{
|
||||
id: "reports",
|
||||
name: "Reports & Insights",
|
||||
reason: "Data-driven decision making with comprehensive reports",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "integrations",
|
||||
name: "Integrations",
|
||||
reason: "Connect with external tools and services",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "search",
|
||||
name: "Global Search",
|
||||
reason: "Quick access to any content across the platform",
|
||||
version: "V2",
|
||||
},
|
||||
{
|
||||
id: "empty-states",
|
||||
name: "Empty States",
|
||||
reason: "Guide users when no data is available",
|
||||
version: "V1",
|
||||
},
|
||||
];
|
||||
|
||||
// Mock tree structure - User Flows
|
||||
const userFlowsTree = [
|
||||
{
|
||||
id: "authentication",
|
||||
name: "Authentication",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "signup", name: "Sign Up", type: "page", route: "/signup", variations: 3 },
|
||||
{ id: "login", name: "Login", type: "page", route: "/login", variations: 2 },
|
||||
{ id: "forgot-password", name: "Forgot Password", type: "page", route: "/forgot-password", variations: 1 },
|
||||
{ id: "verify-email", name: "Verify Email", type: "page", route: "/verify-email", variations: 1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "onboarding",
|
||||
name: "Onboarding",
|
||||
type: "folder",
|
||||
children: [
|
||||
{ id: "welcome", name: "Welcome", type: "page", route: "/onboarding/welcome", variations: 2 },
|
||||
{ id: "setup-profile", name: "Setup Profile", type: "page", route: "/onboarding/profile", variations: 2 },
|
||||
{ id: "preferences", name: "Preferences", type: "page", route: "/onboarding/preferences", variations: 1 },
|
||||
{ id: "complete", name: "Complete", type: "page", route: "/onboarding/complete", variations: 1 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// AI-suggested flows/screens
|
||||
const suggestedFlows = [
|
||||
{
|
||||
id: "password-reset",
|
||||
name: "Password Reset Flow",
|
||||
reason: "Users need a complete password reset journey",
|
||||
version: "V1",
|
||||
screens: [
|
||||
{ name: "Reset Request" },
|
||||
{ name: "Check Email" },
|
||||
{ name: "New Password" },
|
||||
{ name: "Success" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "email-verification",
|
||||
name: "Email Verification Flow",
|
||||
reason: "Enhance security with multi-step verification",
|
||||
version: "V2",
|
||||
screens: [
|
||||
{ name: "Verification Sent" },
|
||||
{ name: "Enter Code" },
|
||||
{ name: "Verified" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "two-factor-setup",
|
||||
name: "Two-Factor Auth Setup",
|
||||
reason: "Add additional security layer for users",
|
||||
version: "V2",
|
||||
screens: [
|
||||
{ name: "Enable 2FA" },
|
||||
{ name: "Setup Authenticator" },
|
||||
{ name: "Verify Code" },
|
||||
{ name: "Backup Codes" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const DESIGN_NAV_ITEMS = [
|
||||
{ title: "Core Screens", icon: LayoutGrid, href: "#screens" },
|
||||
{ title: "User Flows", icon: Workflow, href: "#flows" },
|
||||
{ title: "Style Guide", icon: Palette, href: "#style-guide" },
|
||||
];
|
||||
|
||||
export default function UIUXPage({ params }: { params: Promise<{ projectId: string }> }) {
|
||||
const { projectId } = use(params);
|
||||
const pathname = usePathname();
|
||||
const workspace = pathname.split('/')[1]; // quick hack to get workspace
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
// GitHub connection state
|
||||
const [isGithubConnected, setIsGithubConnected] = useState(false);
|
||||
const [githubRepo, setGithubRepo] = useState<string | null>(null);
|
||||
const [lastSyncTime, setLastSyncTime] = useState<string | null>(null);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
|
||||
// Tree view state
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(["authentication", "dashboard"]));
|
||||
|
||||
const toggleFolder = (folderId: string) => {
|
||||
const newExpanded = new Set(expandedFolders);
|
||||
if (newExpanded.has(folderId)) {
|
||||
newExpanded.delete(folderId);
|
||||
} else {
|
||||
newExpanded.add(folderId);
|
||||
}
|
||||
setExpandedFolders(newExpanded);
|
||||
};
|
||||
|
||||
const handleConnectGithub = async () => {
|
||||
toast.info("Opening GitHub OAuth...");
|
||||
setTimeout(() => {
|
||||
setIsGithubConnected(true);
|
||||
setGithubRepo("username/repo-name");
|
||||
toast.success("GitHub connected!", {
|
||||
description: "Click Sync to scan your repository",
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleSyncRepository = async () => {
|
||||
setIsSyncing(true);
|
||||
|
||||
try {
|
||||
toast.info("Syncing repository...", {
|
||||
description: "AI is analyzing your codebase",
|
||||
});
|
||||
|
||||
const response = await fetch('/api/github/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
projectId,
|
||||
repo: githubRepo,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to sync repository');
|
||||
}
|
||||
|
||||
setLastSyncTime(new Date().toISOString());
|
||||
toast.success("Repository synced!", {
|
||||
description: `Found ${data.pageCount} pages`,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing repository:', error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to sync repository");
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!prompt.trim()) {
|
||||
toast.error("Please enter a design prompt");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v0/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
style: selectedStyle,
|
||||
projectId,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to generate design');
|
||||
}
|
||||
|
||||
toast.success("Design generated successfully!", {
|
||||
description: "Opening in v0...",
|
||||
action: {
|
||||
label: "View",
|
||||
onClick: () => window.open(data.webUrl, '_blank'),
|
||||
},
|
||||
});
|
||||
|
||||
window.open(data.webUrl, '_blank');
|
||||
|
||||
setPrompt("");
|
||||
setSelectedStyle(null);
|
||||
} catch (error) {
|
||||
console.error('Error generating design:', error);
|
||||
toast.error(error instanceof Error ? error.message : "Failed to generate design");
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sidebarItems = DESIGN_NAV_ITEMS.map((item) => {
|
||||
const fullHref = `/${workspace}/project/${projectId}/design${item.href}`;
|
||||
return {
|
||||
...item,
|
||||
href: fullHref,
|
||||
isActive: pathname === fullHref || pathname.startsWith(fullHref),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
items: sidebarItems,
|
||||
}}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* GitHub Connection / Sync */}
|
||||
<div className="flex items-center justify-between p-4 rounded-lg border bg-card">
|
||||
{!isGithubConnected ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Github className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Connect Repository</p>
|
||||
<p className="text-xs text-muted-foreground">Sync your GitHub repo to detect pages</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleConnectGithub} size="sm">
|
||||
<Github className="h-4 w-4 mr-2" />
|
||||
Connect
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Github className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{githubRepo}</p>
|
||||
{lastSyncTime && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Synced {new Date(lastSyncTime).toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleSyncRepository}
|
||||
disabled={isSyncing}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{isSyncing ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Syncing
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Sync
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Screens - Split into two columns */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Core Product */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Core Product</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{coreProductTree.map((folder) => (
|
||||
<div key={folder.id}>
|
||||
{/* Folder */}
|
||||
<button
|
||||
onClick={() => toggleFolder(folder.id)}
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm font-medium"
|
||||
>
|
||||
{expandedFolders.has(folder.id) ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium truncate">{folder.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{folder.children.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Pages in folder */}
|
||||
{expandedFolders.has(folder.id) && (
|
||||
<div className="ml-6 space-y-0.5 mt-0.5">
|
||||
{folder.children.map((page: any) => (
|
||||
<button
|
||||
key={page.id}
|
||||
className="flex items-center justify-between gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm group"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileText className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="truncate">{page.name}</span>
|
||||
</div>
|
||||
{page.variations > 0 && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{page.variations}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* AI Suggested Screens */}
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">AI Suggested</h3>
|
||||
</div>
|
||||
|
||||
{suggestedCoreScreens.map((screen) => (
|
||||
<div key={screen.id} className="px-3 py-2.5 rounded-md border border-dashed border-primary/30 bg-primary/5 hover:bg-primary/10 transition-colors">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Sparkles className="h-4 w-4 text-primary shrink-0" />
|
||||
<div className="font-medium text-sm text-primary truncate">{screen.name}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{screen.version}
|
||||
</Badge>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 text-xs"
|
||||
onClick={() => {
|
||||
toast.success("Generating screen...", {
|
||||
description: `Creating ${screen.name}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3 mr-1" />
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* User Flows */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Flows</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
{userFlowsTree.map((folder) => (
|
||||
<div key={folder.id}>
|
||||
{/* Folder */}
|
||||
<button
|
||||
onClick={() => toggleFolder(folder.id)}
|
||||
className="flex items-center gap-2 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm font-medium"
|
||||
>
|
||||
{expandedFolders.has(folder.id) ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<Folder className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium truncate">{folder.name}</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{folder.children.length} steps
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Pages in folder - with flow indicators */}
|
||||
{expandedFolders.has(folder.id) && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5">
|
||||
{folder.children.map((page: any, index: number) => (
|
||||
<div key={page.id}>
|
||||
<button
|
||||
className="flex items-center justify-between gap-3 w-full px-3 py-1.5 rounded-md hover:bg-accent transition-colors text-sm group"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-primary/10 text-primary text-xs font-semibold shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="truncate">{page.name}</span>
|
||||
</div>
|
||||
{page.variations > 0 && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{page.variations}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* AI Suggested Flows */}
|
||||
<Separator className="my-3" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">AI Suggested</h3>
|
||||
</div>
|
||||
|
||||
{suggestedFlows.map((flow) => (
|
||||
<div key={flow.id} className="space-y-1">
|
||||
<button
|
||||
onClick={() => toggleFolder(`suggested-${flow.id}`)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2.5 rounded-md border border-dashed border-primary/30 bg-primary/5 hover:bg-primary/10 transition-colors text-sm"
|
||||
>
|
||||
{expandedFolders.has(`suggested-${flow.id}`) ? (
|
||||
<ChevronDown className="h-4 w-4 text-primary" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<div className="font-medium text-primary truncate">{flow.name}</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
{flow.version}
|
||||
</Badge>
|
||||
<span className="text-xs text-primary shrink-0">
|
||||
{flow.screens.length} screens
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Suggested screens in flow */}
|
||||
{expandedFolders.has(`suggested-${flow.id}`) && (
|
||||
<div className="ml-6 mt-0.5 space-y-0.5">
|
||||
{flow.screens.map((screen: any, index: number) => (
|
||||
<div key={index}>
|
||||
<div className="flex items-center gap-3 px-3 py-1.5 rounded-md border border-dashed text-sm">
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded-full bg-muted text-muted-foreground text-xs font-semibold shrink-0">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="font-medium text-sm truncate">{screen.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Generate button */}
|
||||
<div className="pt-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
toast.success("Generating flow...", {
|
||||
description: `Creating ${flow.screens.length} screens for ${flow.name}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 mr-2" />
|
||||
Generate This Flow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user