559 lines
21 KiB
TypeScript
559 lines
21 KiB
TypeScript
"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>
|
|
);
|
|
}
|