VIBN Frontend for Coolify deployment
This commit is contained in:
355
components/layout/PAGE_TEMPLATE_GUIDE.md
Normal file
355
components/layout/PAGE_TEMPLATE_GUIDE.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Page Template Guide
|
||||
|
||||
A consistent, reusable page layout system for all pages in the application.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Consistent Layout**: Left rail + sidebar + main content area
|
||||
- ✅ **Responsive Design**: Works on all screen sizes
|
||||
- ✅ **Sidebar Navigation**: Built-in support for left sidebar with active states
|
||||
- ✅ **Hero Section**: Optional hero banner with icon, title, description, and actions
|
||||
- ✅ **Utility Components**: Pre-built sections, cards, grids, and empty states
|
||||
- ✅ **Type-safe**: Full TypeScript support
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { PageTemplate } from "@/components/layout/page-template";
|
||||
import { Home } from "lucide-react";
|
||||
|
||||
export default function MyPage() {
|
||||
return (
|
||||
<PageTemplate
|
||||
hero={{
|
||||
icon: Home,
|
||||
title: "My Page Title",
|
||||
description: "A brief description of what this page does",
|
||||
}}
|
||||
>
|
||||
<div>Your page content here</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## With Sidebar Navigation
|
||||
|
||||
```tsx
|
||||
import { PageTemplate } from "@/components/layout/page-template";
|
||||
import { Home, Settings, Users } from "lucide-react";
|
||||
|
||||
export default function MyPage() {
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
title: "My Section",
|
||||
description: "Navigate through options",
|
||||
items: [
|
||||
{ title: "Home", icon: Home, href: "/home", isActive: true },
|
||||
{ title: "Settings", icon: Settings, href: "/settings" },
|
||||
{ title: "Users", icon: Users, href: "/users", badge: "3" },
|
||||
],
|
||||
footer: <p className="text-xs">Footer content</p>,
|
||||
}}
|
||||
hero={{
|
||||
icon: Home,
|
||||
title: "My Page",
|
||||
description: "Page description",
|
||||
actions: [
|
||||
{
|
||||
label: "Create New",
|
||||
onClick: () => console.log("Create"),
|
||||
icon: Plus,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
<div>Your content</div>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Using Utility Components
|
||||
|
||||
### PageSection
|
||||
|
||||
Organized content sections with optional titles and actions:
|
||||
|
||||
```tsx
|
||||
import { PageSection } from "@/components/layout/page-template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
<PageSection
|
||||
title="Recent Activity"
|
||||
description="Your latest updates"
|
||||
headerAction={<Button size="sm">View All</Button>}
|
||||
>
|
||||
<div>Section content</div>
|
||||
</PageSection>
|
||||
```
|
||||
|
||||
### PageCard
|
||||
|
||||
Styled cards with consistent padding and hover effects:
|
||||
|
||||
```tsx
|
||||
import { PageCard } from "@/components/layout/page-template";
|
||||
|
||||
<PageCard padding="lg" hover>
|
||||
<h3>Card Title</h3>
|
||||
<p>Card content</p>
|
||||
</PageCard>
|
||||
```
|
||||
|
||||
### PageGrid
|
||||
|
||||
Responsive grid layouts:
|
||||
|
||||
```tsx
|
||||
import { PageGrid } from "@/components/layout/page-template";
|
||||
|
||||
<PageGrid cols={3}>
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
<div>Item 3</div>
|
||||
</PageGrid>
|
||||
```
|
||||
|
||||
### PageEmptyState
|
||||
|
||||
Empty states with icon, title, description, and action:
|
||||
|
||||
```tsx
|
||||
import { PageEmptyState } from "@/components/layout/page-template";
|
||||
import { Inbox } from "lucide-react";
|
||||
|
||||
<PageEmptyState
|
||||
icon={Inbox}
|
||||
title="No messages yet"
|
||||
description="Start a conversation to see messages here"
|
||||
action={{
|
||||
label: "New Message",
|
||||
onClick: () => console.log("Create message"),
|
||||
icon: Plus,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a full example combining all components:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import { Home, Settings, Users, Plus, Mail } from "lucide-react";
|
||||
import {
|
||||
PageTemplate,
|
||||
PageSection,
|
||||
PageCard,
|
||||
PageGrid,
|
||||
PageEmptyState,
|
||||
} from "@/components/layout/page-template";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const params = useParams();
|
||||
const workspace = params.workspace as string;
|
||||
const projectId = params.projectId as string;
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
sidebar={{
|
||||
title: "Dashboard",
|
||||
description: "Overview and insights",
|
||||
items: [
|
||||
{
|
||||
title: "Overview",
|
||||
icon: Home,
|
||||
href: `/${workspace}/project/${projectId}/overview`,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
icon: Settings,
|
||||
href: `/${workspace}/project/${projectId}/settings`,
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
icon: Users,
|
||||
href: `/${workspace}/project/${projectId}/users`,
|
||||
badge: "12",
|
||||
},
|
||||
],
|
||||
footer: (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last updated 5 minutes ago
|
||||
</p>
|
||||
),
|
||||
}}
|
||||
hero={{
|
||||
icon: Home,
|
||||
title: "Dashboard",
|
||||
description: "Welcome back! Here's what's happening.",
|
||||
actions: [
|
||||
{
|
||||
label: "Create Report",
|
||||
onClick: () => console.log("Create report"),
|
||||
icon: Plus,
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{/* Stats Grid */}
|
||||
<PageSection title="Quick Stats">
|
||||
<PageGrid cols={4}>
|
||||
<PageCard>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Total Users
|
||||
</h3>
|
||||
<p className="text-3xl font-bold mt-2">1,234</p>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Active Sessions
|
||||
</h3>
|
||||
<p className="text-3xl font-bold mt-2">89</p>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Revenue
|
||||
</h3>
|
||||
<p className="text-3xl font-bold mt-2">$12,345</p>
|
||||
</PageCard>
|
||||
<PageCard>
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Conversion
|
||||
</h3>
|
||||
<p className="text-3xl font-bold mt-2">3.2%</p>
|
||||
</PageCard>
|
||||
</PageGrid>
|
||||
</PageSection>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<PageSection
|
||||
title="Recent Activity"
|
||||
description="Latest updates from your team"
|
||||
>
|
||||
<PageCard>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">User signed up</p>
|
||||
<p className="text-xs text-muted-foreground">2 minutes ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
|
||||
{/* Empty State Example */}
|
||||
<PageSection title="Messages">
|
||||
<PageCard>
|
||||
<PageEmptyState
|
||||
icon={Mail}
|
||||
title="No messages yet"
|
||||
description="When you receive messages, they'll appear here"
|
||||
action={{
|
||||
label: "Send Message",
|
||||
onClick: () => console.log("Send"),
|
||||
icon: Plus,
|
||||
}}
|
||||
/>
|
||||
</PageCard>
|
||||
</PageSection>
|
||||
</PageTemplate>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props Reference
|
||||
|
||||
### PageTemplate
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `children` | `ReactNode` | Main content |
|
||||
| `sidebar` | `object` | Optional sidebar configuration |
|
||||
| `hero` | `object` | Optional hero section configuration |
|
||||
| `containerWidth` | `"default" \| "wide" \| "full"` | Content container width (default: "default") |
|
||||
| `className` | `string` | Custom wrapper class |
|
||||
| `contentClassName` | `string` | Custom content class |
|
||||
|
||||
### Sidebar Config
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `title` | `string` | Sidebar title |
|
||||
| `description` | `string` | Optional subtitle |
|
||||
| `items` | `array` | Navigation items |
|
||||
| `footer` | `ReactNode` | Optional footer content |
|
||||
|
||||
### Hero Config
|
||||
|
||||
| Prop | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `icon` | `LucideIcon` | Optional icon |
|
||||
| `title` | `string` | Page title |
|
||||
| `description` | `string` | Optional description |
|
||||
| `actions` | `array` | Optional action buttons |
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Consistent Icons**: Always use Lucide icons for consistency
|
||||
2. **Active States**: Pass `isActive` to sidebar items to highlight current page
|
||||
3. **Responsive**: Grid and card components are responsive by default
|
||||
4. **Accessibility**: All components include proper ARIA attributes
|
||||
5. **Performance**: Use "use client" only when you need client-side interactivity
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
To migrate an existing page:
|
||||
|
||||
1. Import `PageTemplate` and utility components
|
||||
2. Wrap content in `<PageTemplate>`
|
||||
3. Move navigation to `sidebar` prop
|
||||
4. Move page header to `hero` prop
|
||||
5. Replace div sections with `<PageSection>`
|
||||
6. Replace card divs with `<PageCard>`
|
||||
7. Use `<PageGrid>` for responsive grids
|
||||
|
||||
Before:
|
||||
```tsx
|
||||
<div className="flex h-full">
|
||||
<div className="w-64 border-r">
|
||||
{/* sidebar */}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{/* content */}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
After:
|
||||
```tsx
|
||||
<PageTemplate sidebar={{...}} hero={{...}}>
|
||||
{/* content */}
|
||||
</PageTemplate>
|
||||
```
|
||||
|
||||
63
components/layout/app-shell.tsx
Normal file
63
components/layout/app-shell.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState, useMemo } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LeftRail } from "./left-rail";
|
||||
import { RightPanel } from "./right-panel";
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode;
|
||||
workspace: string;
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export function AppShell({ children, workspace, projectId, projectName }: AppShellProps) {
|
||||
const pathname = usePathname();
|
||||
const [activeSection, setActiveSection] = useState<string>("home");
|
||||
|
||||
// Derive active section from pathname
|
||||
const derivedSection = useMemo(() => {
|
||||
if (pathname.includes('/v_ai_chat')) {
|
||||
return 'ai-chat';
|
||||
} else if (pathname.includes('/overview')) {
|
||||
return 'home';
|
||||
} else if (pathname.includes('/docs')) {
|
||||
return 'docs';
|
||||
} else if (pathname.includes('/plan') || pathname.includes('/timeline-plan')) {
|
||||
return 'plan';
|
||||
} else if (pathname.includes('/design')) {
|
||||
return 'design';
|
||||
} else if (pathname.includes('/tech')) {
|
||||
return 'tech';
|
||||
} else if (pathname.includes('/journey')) {
|
||||
return 'journey';
|
||||
}
|
||||
return activeSection;
|
||||
}, [pathname, activeSection]);
|
||||
|
||||
const displayProjectName = projectName || "Product";
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-full overflow-hidden bg-background">
|
||||
{/* Left Rail - App Navigation */}
|
||||
<LeftRail
|
||||
key={projectId} // Force re-render when projectId changes
|
||||
activeSection={derivedSection}
|
||||
onSectionChange={setActiveSection}
|
||||
projectName={displayProjectName}
|
||||
projectId={projectId}
|
||||
workspace={workspace}
|
||||
/>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex flex-col overflow-y-auto">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
{/* Right Panel - Activity & AI Chat */}
|
||||
<RightPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
161
components/layout/connect-sources-modal.tsx
Normal file
161
components/layout/connect-sources-modal.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Github, Sparkles, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { CursorIcon } from "@/components/icons/custom-icons";
|
||||
|
||||
interface ConnectSourcesModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
// Mock connection states - these would come from your database in production
|
||||
const useConnectionStates = () => {
|
||||
const [connections, setConnections] = useState({
|
||||
vibn: false,
|
||||
chatgpt: false,
|
||||
github: false,
|
||||
v0: false,
|
||||
});
|
||||
|
||||
return { connections, setConnections };
|
||||
};
|
||||
|
||||
export function ConnectSourcesModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
projectId,
|
||||
}: ConnectSourcesModalProps) {
|
||||
const { connections, setConnections } = useConnectionStates();
|
||||
|
||||
const handleConnect = async (source: "vibn" | "chatgpt" | "github" | "v0") => {
|
||||
// Mock connection logic - replace with actual OAuth/API integration
|
||||
const sourceName = source === "vibn" ? "Vib'n Extension" : source === "chatgpt" ? "ChatGPT" : source === "github" ? "GitHub" : "v0";
|
||||
toast.success(`Connecting to ${sourceName}...`);
|
||||
|
||||
// Simulate connection delay
|
||||
setTimeout(() => {
|
||||
setConnections((prev) => ({ ...prev, [source]: true }));
|
||||
toast.success(`${sourceName} connected successfully!`);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleDisconnect = (source: "vibn" | "chatgpt" | "github" | "v0") => {
|
||||
const sourceName = source === "vibn" ? "Vib'n Extension" : source === "chatgpt" ? "ChatGPT" : source === "github" ? "GitHub" : "v0";
|
||||
setConnections((prev) => ({ ...prev, [source]: false }));
|
||||
toast.success(`${sourceName} disconnected`);
|
||||
};
|
||||
|
||||
const sources = [
|
||||
{
|
||||
id: "vibn" as const,
|
||||
name: "Vib'n Extension",
|
||||
description: "Connect the Vib'n extension with Cursor for seamless development tracking",
|
||||
icon: CursorIcon,
|
||||
color: "text-foreground",
|
||||
bgColor: "bg-primary/10",
|
||||
},
|
||||
{
|
||||
id: "chatgpt" as const,
|
||||
name: "ChatGPT",
|
||||
description: "Connect your ChatGPT project for AI-powered insights and context",
|
||||
icon: Sparkles,
|
||||
color: "text-green-500",
|
||||
bgColor: "bg-green-500/10",
|
||||
},
|
||||
{
|
||||
id: "github" as const,
|
||||
name: "GitHub",
|
||||
description: "Sync your repository to track code changes and generate screens",
|
||||
icon: Github,
|
||||
color: "text-foreground",
|
||||
bgColor: "bg-foreground/10",
|
||||
},
|
||||
{
|
||||
id: "v0" as const,
|
||||
name: "v0",
|
||||
description: "Connect v0 to generate and iterate on UI designs",
|
||||
icon: Sparkles,
|
||||
color: "text-blue-500",
|
||||
bgColor: "bg-blue-500/10",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect Sources</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect external sources to enhance your product development workflow
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 mt-4">
|
||||
{sources.map((source) => {
|
||||
const Icon = source.icon;
|
||||
const isConnected = connections[source.id];
|
||||
|
||||
return (
|
||||
<Card key={source.id} className={isConnected ? "border-primary" : ""}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-2.5 rounded-lg ${source.bgColor} shrink-0`}>
|
||||
<Icon className={`h-5 w-5 ${source.color}`} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-sm">{source.name}</h3>
|
||||
{isConnected && (
|
||||
<div className="flex items-center gap-1 text-xs text-green-600 bg-green-500/10 px-2 py-0.5 rounded-full">
|
||||
<Check className="h-3 w-3" />
|
||||
Connected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{source.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0">
|
||||
{isConnected ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDisconnect(source.id)}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleConnect(source.id)}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
164
components/layout/left-rail.tsx
Normal file
164
components/layout/left-rail.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
LayoutGrid,
|
||||
Inbox,
|
||||
Users,
|
||||
Receipt,
|
||||
Globe,
|
||||
FileText,
|
||||
MessageCircle,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
User,
|
||||
DollarSign,
|
||||
Key,
|
||||
Palette,
|
||||
Target,
|
||||
Megaphone,
|
||||
ClipboardList,
|
||||
Code2,
|
||||
Sparkles,
|
||||
Cog,
|
||||
GitBranch,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type NavSection = "home" | "ai-chat" | "docs" | "plan" | "design" | "tech" | "journey" | "settings";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ id: "plan" as NavSection, icon: ClipboardList, label: "Plan", href: "/project/{projectId}/plan" },
|
||||
{ id: "docs" as NavSection, icon: FileText, label: "Docs", href: "/project/{projectId}/docs" },
|
||||
{ id: "design" as NavSection, icon: Palette, label: "Design", href: "/project/{projectId}/design" },
|
||||
{ id: "tech" as NavSection, icon: Cog, label: "Tech", href: "/project/{projectId}/tech" },
|
||||
{ id: "journey" as NavSection, icon: GitBranch, label: "Journey", href: "/project/{projectId}/journey" },
|
||||
];
|
||||
|
||||
interface LeftRailProps {
|
||||
activeSection: string;
|
||||
onSectionChange: (section: string) => void;
|
||||
projectName?: string;
|
||||
projectId?: string;
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
export function LeftRail({ activeSection, onSectionChange, projectName, projectId, workspace = 'marks-account' }: LeftRailProps) {
|
||||
return (
|
||||
<div className="flex w-16 flex-col items-center border-r bg-card">
|
||||
{/* Vib'n Logo */}
|
||||
<Link href={`/${workspace}/projects`} className="flex h-14 w-16 items-center justify-center border-b">
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="Vib'n"
|
||||
className="h-10 w-10 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{/* Main Navigation Items */}
|
||||
<div className="flex flex-col gap-2 w-full items-center">
|
||||
{/* AI Chat - Always visible */}
|
||||
{projectName && projectId && (
|
||||
<Link
|
||||
href={`/${workspace}/project/${projectId}/v_ai_chat`}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
|
||||
activeSection === "ai-chat"
|
||||
? "text-primary bg-primary/5"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
title="AI Chat"
|
||||
>
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">AI Chat</span>
|
||||
{activeSection === "ai-chat" && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{/* Other Nav Items */}
|
||||
{NAV_ITEMS.map((item) => {
|
||||
if (!projectId) return null;
|
||||
const fullHref = `/${workspace}${item.href.replace('{projectId}', projectId)}`;
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={fullHref}
|
||||
onClick={() => onSectionChange(item.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
|
||||
activeSection === item.id
|
||||
? "text-primary bg-primary/5"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
title={item.label}
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">{item.label}</span>
|
||||
{activeSection === item.id && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom Items */}
|
||||
<div className="mt-auto flex flex-col gap-1 w-full items-center pb-4">
|
||||
<Separator className="w-8 mb-1" />
|
||||
|
||||
<Link href={`/${workspace}/keys`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
title="API Keys"
|
||||
>
|
||||
<Key className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={`/${workspace}/connections`}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
title="Settings & Connections"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10"
|
||||
title="Help - Coming Soon"
|
||||
onClick={() => toast.info("Help Center - Coming Soon")}
|
||||
>
|
||||
<HelpCircle className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 rounded-full"
|
||||
title="Profile - Coming Soon"
|
||||
onClick={() => toast.info("Profile Settings - Coming Soon")}
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
189
components/layout/mcp-connect-modal.tsx
Normal file
189
components/layout/mcp-connect-modal.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ExternalLink, Copy, CheckCircle2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface MCPConnectModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export function MCPConnectModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
projectId,
|
||||
}: MCPConnectModalProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const mcpUrl = `https://api.vibn.co/mcp/projects/${projectId}`;
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(mcpUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br from-green-500/20 to-emerald-500/20">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
|
||||
fill="currentColor"
|
||||
className="text-green-600"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl">Connect ChatGPT</DialogTitle>
|
||||
<Badge variant="secondary" className="mt-1 text-xs">MCP Protocol</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription>
|
||||
Link your ChatGPT to this project for real-time sync and AI-powered updates
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* Step 1 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
|
||||
1
|
||||
</div>
|
||||
<h3 className="font-semibold">Copy your MCP Server URL</h3>
|
||||
</div>
|
||||
<div className="ml-8 space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={mcpUrl}
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This unique URL connects ChatGPT to your project
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
|
||||
2
|
||||
</div>
|
||||
<h3 className="font-semibold">Add Connector in ChatGPT</h3>
|
||||
</div>
|
||||
<div className="ml-8 space-y-2">
|
||||
<ol className="text-sm space-y-2 text-muted-foreground list-decimal list-inside">
|
||||
<li>Open ChatGPT settings</li>
|
||||
<li>Navigate to <strong className="text-foreground">Connectors</strong></li>
|
||||
<li>Click <strong className="text-foreground">New Connector</strong></li>
|
||||
<li>Paste the MCP Server URL</li>
|
||||
<li>Select <strong className="text-foreground">OAuth</strong> authentication</li>
|
||||
</ol>
|
||||
<Button variant="outline" size="sm" className="mt-3" asChild>
|
||||
<a href="https://chatgpt.com/settings" target="_blank" rel="noopener noreferrer">
|
||||
Open ChatGPT Settings
|
||||
<ExternalLink className="ml-2 h-3 w-3" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 3 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary/10 text-sm font-medium">
|
||||
3
|
||||
</div>
|
||||
<h3 className="font-semibold">Authorize Access</h3>
|
||||
</div>
|
||||
<div className="ml-8">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
ChatGPT will request permission to:
|
||||
</p>
|
||||
<ul className="mt-2 text-sm space-y-1 text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-600">✓</span>
|
||||
Read your project vision and progress
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-600">✓</span>
|
||||
Add features and tasks
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="text-green-600">✓</span>
|
||||
Update documentation
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="rounded-lg bg-blue-500/10 border border-blue-500/20 p-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="text-blue-600 shrink-0">ℹ️</div>
|
||||
<div className="text-sm space-y-1">
|
||||
<p className="font-medium text-foreground">What happens after connecting?</p>
|
||||
<p className="text-muted-foreground">
|
||||
You'll be able to chat with ChatGPT about your project, and it will automatically
|
||||
sync updates to your Vib'n workspace. Plan features, discuss architecture, and track
|
||||
progress - all seamlessly connected.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-between pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="outline" asChild>
|
||||
<a
|
||||
href="https://platform.openai.com/docs/mcp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View MCP Docs
|
||||
<ExternalLink className="ml-2 h-3 w-3" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
38
components/layout/page-header.tsx
Normal file
38
components/layout/page-header.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronRight, Info } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface PageHeaderProps {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
projectEmoji?: string;
|
||||
pageName: string;
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
projectId,
|
||||
projectName = "AI Proxy",
|
||||
projectEmoji = "🤖",
|
||||
pageName,
|
||||
}: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex h-12 items-center justify-between border-b bg-card/50 px-6">
|
||||
{/* Breadcrumbs */}
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-base">{projectEmoji}</span>
|
||||
<span className="font-medium">{projectName}</span>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">{pageName}</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Info className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
306
components/layout/page-template.tsx
Normal file
306
components/layout/page-template.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
|
||||
interface PageTemplateProps {
|
||||
children: ReactNode;
|
||||
|
||||
// Sidebar configuration
|
||||
sidebar?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
items: Array<{
|
||||
title: string;
|
||||
icon: LucideIcon;
|
||||
href: string;
|
||||
isActive?: boolean;
|
||||
badge?: string | number;
|
||||
}>;
|
||||
footer?: ReactNode;
|
||||
customContent?: ReactNode;
|
||||
};
|
||||
|
||||
// Hero section configuration
|
||||
hero?: {
|
||||
icon?: LucideIcon;
|
||||
iconBgColor?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: Array<{
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: "default" | "outline" | "ghost" | "secondary";
|
||||
icon?: LucideIcon;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Container width
|
||||
containerWidth?: "default" | "wide" | "full";
|
||||
|
||||
// Custom classes
|
||||
className?: string;
|
||||
contentClassName?: string;
|
||||
}
|
||||
|
||||
export function PageTemplate({
|
||||
children,
|
||||
sidebar,
|
||||
hero,
|
||||
containerWidth = "default",
|
||||
className,
|
||||
contentClassName,
|
||||
}: PageTemplateProps) {
|
||||
const maxWidthClass = {
|
||||
default: "max-w-5xl",
|
||||
wide: "max-w-7xl",
|
||||
full: "max-w-none",
|
||||
}[containerWidth];
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full w-full overflow-hidden", className)}>
|
||||
{/* Left Sidebar Navigation (if provided) */}
|
||||
{sidebar && (
|
||||
<div className="w-64 border-r bg-muted/30 py-4">
|
||||
{/* Sidebar Header */}
|
||||
{sidebar.title && (
|
||||
<div className="pb-4 border-b mb-4">
|
||||
<h2 className="text-lg font-semibold">{sidebar.title}</h2>
|
||||
{sidebar.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{sidebar.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Div: Navigation Items */}
|
||||
<div className="px-3 py-4 space-y-1">
|
||||
{sidebar.items.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = item.isActive;
|
||||
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 px-2 py-1.5 rounded-md text-sm transition-all group",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Icon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary shrink-0 font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t my-4" />
|
||||
|
||||
{/* Bottom Div: Custom Content */}
|
||||
{sidebar.customContent && (
|
||||
<div className="px-3 py-4">
|
||||
{sidebar.customContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Hero Section (if provided) */}
|
||||
{hero && (
|
||||
<div className="border-b bg-gradient-to-br from-primary/5 to-background">
|
||||
<div className={cn(maxWidthClass, "mx-auto px-8 py-12")}>
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="flex items-center gap-4 min-w-0 flex-1">
|
||||
{hero.icon && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-12 w-12 rounded-xl flex items-center justify-center shrink-0",
|
||||
hero.iconBgColor || "bg-primary/10"
|
||||
)}
|
||||
>
|
||||
<hero.icon className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="text-3xl font-bold truncate">{hero.title}</h1>
|
||||
{hero.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{hero.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Actions */}
|
||||
{hero.actions && hero.actions.length > 0 && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{hero.actions.map((action, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant || "default"}
|
||||
size="sm"
|
||||
>
|
||||
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Page Content */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className={cn(maxWidthClass, "mx-auto px-6 py-6", contentClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Utility Components for common page patterns
|
||||
|
||||
interface PageSectionProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
headerAction?: ReactNode;
|
||||
}
|
||||
|
||||
export function PageSection({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
className,
|
||||
headerAction,
|
||||
}: PageSectionProps) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{(title || description || headerAction) && (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
{title && <h2 className="text-lg font-semibold">{title}</h2>}
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{headerAction && <div>{headerAction}</div>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageCardProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
padding?: "sm" | "md" | "lg";
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
export function PageCard({
|
||||
children,
|
||||
className,
|
||||
padding = "md",
|
||||
hover = false,
|
||||
}: PageCardProps) {
|
||||
const paddingClass = {
|
||||
sm: "p-4",
|
||||
md: "p-6",
|
||||
lg: "p-8",
|
||||
}[padding];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border rounded-lg bg-card",
|
||||
paddingClass,
|
||||
hover && "hover:border-primary hover:shadow-md transition-all",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageGridProps {
|
||||
children: ReactNode;
|
||||
cols?: 1 | 2 | 3 | 4;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageGrid({ children, cols = 2, className }: PageGridProps) {
|
||||
const colsClass = {
|
||||
1: "grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-3",
|
||||
4: "md:grid-cols-2 lg:grid-cols-4",
|
||||
}[cols];
|
||||
|
||||
return (
|
||||
<div className={cn("grid grid-cols-1 gap-4", colsClass, className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PageEmptyStateProps {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: LucideIcon;
|
||||
};
|
||||
}
|
||||
|
||||
export function PageEmptyState({
|
||||
icon: Icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
}: PageEmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4 text-center">
|
||||
<div className="rounded-full bg-muted p-6 mb-4">
|
||||
<Icon className="h-12 w-12 text-muted-foreground" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-muted-foreground mb-6 max-w-md">{description}</p>
|
||||
)}
|
||||
{action && (
|
||||
<Button onClick={action.onClick}>
|
||||
{action.icon && <action.icon className="h-4 w-4 mr-2" />}
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
655
components/layout/project-sidebar.tsx
Normal file
655
components/layout/project-sidebar.tsx
Normal file
@@ -0,0 +1,655 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Target,
|
||||
ListChecks,
|
||||
Palette,
|
||||
Code2,
|
||||
Server,
|
||||
Zap,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Github,
|
||||
MessageSquare,
|
||||
Image,
|
||||
Globe,
|
||||
FolderOpen,
|
||||
Inbox,
|
||||
Users,
|
||||
Eye,
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { MCPConnectModal } from "./mcp-connect-modal";
|
||||
import { ConnectSourcesModal } from "./connect-sources-modal";
|
||||
import { OpenAIIcon, V0Icon, CursorIcon } from "@/components/icons/custom-icons";
|
||||
|
||||
interface ProjectSidebarProps {
|
||||
projectId: string;
|
||||
activeSection?: string; // From left rail: 'projects', 'inbox', 'clients', etc.
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
// Map section IDs to display names
|
||||
const SECTION_NAMES: Record<string, string> = {
|
||||
home: 'Home',
|
||||
product: 'Product',
|
||||
site: 'Site',
|
||||
pricing: 'Pricing',
|
||||
content: 'Content',
|
||||
social: 'Social',
|
||||
inbox: 'Inbox',
|
||||
people: 'People',
|
||||
settings: 'Settings',
|
||||
};
|
||||
|
||||
// Section-specific navigation items
|
||||
const SECTION_ITEMS: Record<string, Array<{title: string; icon: any; href: string}>> = {
|
||||
home: [
|
||||
// { title: "Vision", icon: Eye, href: "/vision" }, // Hidden per user request
|
||||
{ title: "Context", icon: FolderOpen, href: "/context" },
|
||||
],
|
||||
product: [
|
||||
{ title: "Product Vision", icon: Target, href: "/plan" },
|
||||
{ title: "Progress", icon: ListChecks, href: "/progress" },
|
||||
{ title: "UI UX", icon: Palette, href: "/design" },
|
||||
{ title: "Code", icon: Code2, href: "/code" },
|
||||
{ title: "Deployment", icon: Server, href: "/deployment" },
|
||||
{ title: "Automation", icon: Zap, href: "/automation" },
|
||||
],
|
||||
site: [
|
||||
{ title: "Pages", icon: Globe, href: "/site/pages" },
|
||||
{ title: "Templates", icon: Palette, href: "/site/templates" },
|
||||
{ title: "Settings", icon: Target, href: "/site/settings" },
|
||||
],
|
||||
pricing: [
|
||||
{ title: "Plans", icon: Target, href: "/pricing/plans" },
|
||||
{ title: "Billing", icon: Code2, href: "/pricing/billing" },
|
||||
{ title: "Invoices", icon: ListChecks, href: "/pricing/invoices" },
|
||||
],
|
||||
content: [
|
||||
{ title: "Blog Posts", icon: Target, href: "/content/blog" },
|
||||
{ title: "Case Studies", icon: Code2, href: "/content/cases" },
|
||||
{ title: "Documentation", icon: ListChecks, href: "/content/docs" },
|
||||
],
|
||||
social: [
|
||||
{ title: "Posts", icon: MessageSquare, href: "/social/posts" },
|
||||
{ title: "Analytics", icon: Target, href: "/social/analytics" },
|
||||
{ title: "Schedule", icon: ListChecks, href: "/social/schedule" },
|
||||
],
|
||||
inbox: [
|
||||
{ title: "All", icon: Inbox, href: "/inbox/all" },
|
||||
{ title: "Unread", icon: Target, href: "/inbox/unread" },
|
||||
{ title: "Archived", icon: ListChecks, href: "/inbox/archived" },
|
||||
],
|
||||
people: [
|
||||
{ title: "Team", icon: Users, href: "/people/team" },
|
||||
{ title: "Clients", icon: Users, href: "/people/clients" },
|
||||
{ title: "Contacts", icon: Users, href: "/people/contacts" },
|
||||
],
|
||||
};
|
||||
|
||||
type ConnectionStatus = 'inactive' | 'connected' | 'live';
|
||||
|
||||
export function ProjectSidebar({ projectId, activeSection = 'projects', workspace = 'marks-account' }: ProjectSidebarProps) {
|
||||
const minWidth = 200;
|
||||
const maxWidth = 500;
|
||||
const [width, setWidth] = useState(minWidth);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [mcpModalOpen, setMcpModalOpen] = useState(false);
|
||||
const [connectModalOpen, setConnectModalOpen] = useState(false);
|
||||
const [isUserFlowsExpanded, setIsUserFlowsExpanded] = useState(true);
|
||||
const [isProductScreensExpanded, setIsProductScreensExpanded] = useState(true);
|
||||
const [getStartedCompleted, setGetStartedCompleted] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Connection states - mock data, would come from API/database in production
|
||||
const [connectionStates, setConnectionStates] = useState<{
|
||||
github: ConnectionStatus;
|
||||
openai: ConnectionStatus;
|
||||
v0: ConnectionStatus;
|
||||
cursor: ConnectionStatus;
|
||||
}>({
|
||||
github: 'connected',
|
||||
openai: 'live',
|
||||
v0: 'inactive',
|
||||
cursor: 'connected',
|
||||
});
|
||||
|
||||
// Helper function to get icon classes based on connection status
|
||||
const getIconClasses = (status: ConnectionStatus) => {
|
||||
switch (status) {
|
||||
case 'inactive':
|
||||
return 'text-muted-foreground/40';
|
||||
case 'connected':
|
||||
return 'text-muted-foreground';
|
||||
case 'live':
|
||||
return 'text-foreground';
|
||||
default:
|
||||
return 'text-muted-foreground/40';
|
||||
}
|
||||
};
|
||||
|
||||
const startResizing = useCallback((e: React.MouseEvent) => {
|
||||
setIsResizing(true);
|
||||
e.preventDefault();
|
||||
}, []);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const resize = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (isResizing) {
|
||||
const newWidth = e.clientX - 64; // Subtract left rail width (64px)
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isResizing]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
// Determine header title based on active section
|
||||
const isVAIPage = pathname?.includes('/v_ai_chat');
|
||||
const headerTitle = isVAIPage ? 'v_ai' : (SECTION_NAMES[activeSection] || 'Home');
|
||||
|
||||
// Get section-specific items
|
||||
const currentSectionItems = SECTION_ITEMS[activeSection] || SECTION_ITEMS['home'];
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
ref={sidebarRef}
|
||||
style={{ width: `${width}px` }}
|
||||
className="relative flex flex-col border-r bg-card/50"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex h-14 items-center justify-between px-4 border-b">
|
||||
<h2 className="font-semibold text-sm">{headerTitle}</h2>
|
||||
{/* Connection icons - only show for Product section */}
|
||||
{activeSection === 'product' && (
|
||||
<div className="flex items-center gap-1">
|
||||
{/* GitHub */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 relative"
|
||||
onClick={() => setConnectModalOpen(true)}
|
||||
>
|
||||
<Github className={cn("h-4 w-4", getIconClasses(connectionStates.github))} />
|
||||
{connectionStates.github === 'live' && (
|
||||
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* OpenAI/ChatGPT */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 relative"
|
||||
onClick={() => setConnectModalOpen(true)}
|
||||
>
|
||||
<OpenAIIcon className={cn("h-4 w-4", getIconClasses(connectionStates.openai))} />
|
||||
{connectionStates.openai === 'live' && (
|
||||
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* v0 */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 relative"
|
||||
onClick={() => setConnectModalOpen(true)}
|
||||
>
|
||||
<V0Icon className={cn("h-4 w-4", getIconClasses(connectionStates.v0))} />
|
||||
{connectionStates.v0 === 'live' && (
|
||||
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Cursor */}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 relative"
|
||||
onClick={() => setConnectModalOpen(true)}
|
||||
>
|
||||
<CursorIcon className={cn("h-4 w-4", getIconClasses(connectionStates.cursor))} />
|
||||
{connectionStates.cursor === 'live' && (
|
||||
<span className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section-Specific Navigation */}
|
||||
<div className="px-2 py-1.5 space-y-0.5">
|
||||
{/* v_ai - Persistent AI Chat - Always show for Home section */}
|
||||
{activeSection === 'home' && (
|
||||
<div className="mb-2">
|
||||
<Link
|
||||
href={`/${workspace}/project/${projectId}/v_ai_chat`}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors",
|
||||
pathname === `/${workspace}/project/${projectId}/v_ai_chat`
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<img src="/vibn-logo-circle.png" alt="v_ai" className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate font-medium">v_ai</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentSectionItems.map((item) => {
|
||||
const href = `/${workspace}/project/${projectId}${item.href}`;
|
||||
const isActive = pathname === href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{item.title}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<Separator className="my-1.5" />
|
||||
|
||||
{/* Context Section - Only show in Home section */}
|
||||
{activeSection === 'home' && !isVAIPage && (
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="px-2 py-2">
|
||||
<div className="flex items-center justify-between px-2 mb-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Context</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||
onClick={() => {
|
||||
// Navigate to context page
|
||||
window.location.href = `/${workspace}/project/${projectId}/context`;
|
||||
}}
|
||||
>
|
||||
<Plus className="h-3 w-3 mr-1" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Context Section - Shows items based on selected project section */}
|
||||
{!isVAIPage && activeSection !== 'home' && (
|
||||
<ScrollArea className="flex-1 px-2">
|
||||
<div className="space-y-1 py-1.5">
|
||||
{/* Show context based on current page */}
|
||||
{pathname.includes('/plan') && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground">VISION SOURCES</h3>
|
||||
<button
|
||||
onClick={() => setMcpModalOpen(true)}
|
||||
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-xs hover:bg-accent transition-colors text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{/* OpenAI Icon (SVG) */}
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="opacity-60"
|
||||
>
|
||||
<path
|
||||
d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span>Connect</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span className="text-xs">📄</span>
|
||||
<span className="truncate">chatgpt-brainstorm.json</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span className="text-xs">📄</span>
|
||||
<span className="truncate">product-notes.md</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm border border-dashed border-primary/30 text-primary hover:bg-primary/5 transition-colors">
|
||||
<span className="text-xs">+</span>
|
||||
<span className="truncate">Upload new file</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pathname.includes('/progress') && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">FILTERS</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>All Tasks</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>In Progress</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>Completed</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pathname.includes('/design') && (
|
||||
<div className="space-y-1.5">
|
||||
{/* Sandbox Indicator */}
|
||||
<div className="px-2 py-1">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground">YOUR SANDBOX</h3>
|
||||
</div>
|
||||
|
||||
{/* User Flows */}
|
||||
<button
|
||||
onClick={() => setIsUserFlowsExpanded(!isUserFlowsExpanded)}
|
||||
className="flex w-full items-center justify-between px-2 py-1 rounded hover:bg-accent transition-colors"
|
||||
>
|
||||
<h3 className="text-xs font-semibold text-muted-foreground">USER FLOWS</h3>
|
||||
{isUserFlowsExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{isUserFlowsExpanded && (
|
||||
<div className="space-y-0.5">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🚀 Onboarding</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>✍️ Signup</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>👋 New User</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Screens */}
|
||||
<button
|
||||
onClick={() => setIsProductScreensExpanded(!isProductScreensExpanded)}
|
||||
className="flex w-full items-center justify-between px-2 py-1 rounded hover:bg-accent transition-colors mt-2.5"
|
||||
>
|
||||
<h3 className="text-xs font-semibold text-muted-foreground">PRODUCT SCREENS</h3>
|
||||
{isProductScreensExpanded ? (
|
||||
<ChevronDown className="h-3 w-3 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{isProductScreensExpanded && (
|
||||
<div className="space-y-0.5">
|
||||
<Link
|
||||
href={`/${projectId}/design/landing-hero`}
|
||||
className={cn(
|
||||
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
|
||||
pathname === `/${projectId}/design/landing-hero`
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>✨</span>
|
||||
<span>Landing Hero</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">3</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${projectId}/design/dashboard`}
|
||||
className={cn(
|
||||
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
|
||||
pathname === `/${projectId}/design/dashboard`
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>📊</span>
|
||||
<span>Dashboard</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">1</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${projectId}/design/pricing`}
|
||||
className={cn(
|
||||
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
|
||||
pathname === `/${projectId}/design/pricing`
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>💳</span>
|
||||
<span>Pricing</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">2</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${projectId}/design/user-profile`}
|
||||
className={cn(
|
||||
"group flex w-full items-center justify-between rounded-md px-2 py-1 text-sm transition-colors",
|
||||
pathname === `/${projectId}/design/user-profile`
|
||||
? "bg-accent text-accent-foreground font-medium"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>👤</span>
|
||||
<span>User Profile</span>
|
||||
</div>
|
||||
<span className="text-xs opacity-0 group-hover:opacity-100 transition-opacity">1</span>
|
||||
</Link>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="px-2 pt-1.5">
|
||||
<button className="flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-primary/30 px-2 py-1.5 text-sm text-primary hover:bg-primary/5 transition-colors">
|
||||
<span>+</span>
|
||||
<span>New Screen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pathname.includes('/code') && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">QUICK LINKS</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>📦 Repository</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🌿 Branches</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🔀 Pull Requests</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pathname.includes('/deployment') && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">ENVIRONMENTS</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🟢 Production</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🟡 Staging</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>🔵 Development</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pathname.includes('/automation') && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">WORKFLOWS</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>⚡ Active Jobs</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>⏰ Scheduled</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors text-muted-foreground">
|
||||
<span>📋 Logs</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Default: Left rail context sections */}
|
||||
{activeSection === 'inbox' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">INBOX</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span className="text-muted-foreground">No new items</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'clients' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">PEOPLE</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Personal Projects</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>VIBN</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'invoices' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">GROW</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>No growth strategies yet</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'site' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">SITE</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Pages</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'content' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">CONTENT</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Blog Posts</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Case Studies</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeSection === 'social' && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="px-2 text-xs font-semibold text-muted-foreground">SOCIAL</h3>
|
||||
<div className="space-y-1">
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Posts</span>
|
||||
</button>
|
||||
<button className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-accent transition-colors">
|
||||
<span>Analytics</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex h-10 items-center justify-between border-t px-4 text-xs text-muted-foreground">
|
||||
<span>v1.0.0</span>
|
||||
<button className="hover:text-foreground transition-colors">
|
||||
Help
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
onMouseDown={startResizing}
|
||||
className="absolute right-0 top-0 h-full w-1 cursor-col-resize hover:bg-primary/20 active:bg-primary/40 transition-colors"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<MCPConnectModal
|
||||
open={mcpModalOpen}
|
||||
onOpenChange={setMcpModalOpen}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<ConnectSourcesModal
|
||||
open={connectModalOpen}
|
||||
onOpenChange={setConnectModalOpen}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
119
components/layout/right-panel.tsx
Normal file
119
components/layout/right-panel.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Sparkles,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Send,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
export function RightPanel() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<div className="relative flex w-12 flex-col items-center border-l bg-card/50 py-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="relative flex w-80 flex-col border-l bg-card/50">
|
||||
{/* Header */}
|
||||
<div className="flex h-12 items-center justify-between px-4 border-b">
|
||||
<h2 className="font-semibold text-sm">AI Chat</h2>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
className="h-7 w-7"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Chat Messages */}
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Empty State */}
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="mb-3 rounded-full bg-primary/10 p-3">
|
||||
<Sparkles className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-medium mb-1">AI Assistant</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-[250px]">
|
||||
Ask questions about your project, get code suggestions, or
|
||||
request documentation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Example Chat Messages (for when conversation exists) */}
|
||||
{/*
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-3">
|
||||
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<Sparkles className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="text-sm bg-muted rounded-lg p-3">
|
||||
How can I help you with your project today?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 flex-row-reverse">
|
||||
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-xs font-medium text-primary-foreground">
|
||||
You
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="text-sm bg-primary text-primary-foreground rounded-lg p-3">
|
||||
What's the current token usage?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Chat Input */}
|
||||
<div className="border-t p-4 space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
placeholder="Ask AI about your project..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
className="min-h-[60px] resize-none"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
// TODO: Send message
|
||||
setMessage("");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="icon" className="shrink-0">
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
150
components/layout/workspace-left-rail.tsx
Normal file
150
components/layout/workspace-left-rail.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
LayoutGrid,
|
||||
Cable,
|
||||
Key,
|
||||
Users,
|
||||
Settings,
|
||||
DollarSign,
|
||||
LogOut,
|
||||
} from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { signOut } from "@/lib/firebase/auth";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface WorkspaceLeftRailProps {
|
||||
activeSection?: string;
|
||||
onSectionChange: (section: string) => void;
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
id: 'projects',
|
||||
label: 'Projects',
|
||||
icon: LayoutGrid,
|
||||
href: '/projects',
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
label: 'Connect',
|
||||
icon: Cable,
|
||||
href: '/connections',
|
||||
},
|
||||
{
|
||||
id: 'keys',
|
||||
label: 'Keys',
|
||||
icon: Key,
|
||||
href: '/keys',
|
||||
},
|
||||
{
|
||||
id: 'costs',
|
||||
label: 'Costs',
|
||||
icon: DollarSign,
|
||||
href: '/costs',
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
label: 'Users',
|
||||
icon: Users,
|
||||
href: '/users',
|
||||
},
|
||||
];
|
||||
|
||||
export function WorkspaceLeftRail({ activeSection = 'projects', onSectionChange }: WorkspaceLeftRailProps) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
// Extract workspace from pathname (e.g., /marks-account/projects -> marks-account)
|
||||
const workspace = pathname?.split('/')[1] || 'marks-account';
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await signOut();
|
||||
toast.success("Signed out successfully");
|
||||
router.push("/auth");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-16 flex-col items-center border-r bg-card">
|
||||
{/* Vib'n Logo */}
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
onClick={() => onSectionChange('projects')}
|
||||
className="flex h-14 w-16 items-center justify-center border-b"
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="Vib'n"
|
||||
className="h-10 w-10 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<div className="pt-4 w-full flex flex-col items-center gap-2">
|
||||
{/* Navigation Items */}
|
||||
<div className="flex flex-col gap-3 w-full items-center">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const fullHref = `/${workspace}${item.href}`;
|
||||
const isActive = activeSection === item.id || pathname?.includes(item.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.id}
|
||||
href={fullHref}
|
||||
onClick={() => onSectionChange(item.id)}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all relative",
|
||||
isActive
|
||||
? "text-primary bg-primary/5"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">{item.label}</span>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 h-10 w-1 bg-primary" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Bottom Items */}
|
||||
<div className="mt-auto flex flex-col gap-1 w-full items-center pb-4">
|
||||
<Separator className="w-8 mb-1" />
|
||||
|
||||
<Link
|
||||
href={`/${workspace}/settings`}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all",
|
||||
"text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">Settings</span>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 w-full py-2 px-2 transition-all",
|
||||
"text-muted-foreground hover:text-foreground hover:bg-accent"
|
||||
)}
|
||||
>
|
||||
<LogOut className="h-5 w-5" />
|
||||
<span className="text-[10px] font-medium">Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user