VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
"use client";
import { useParams, usePathname } from "next/navigation";
import {
ClipboardList,
CheckCircle2,
Circle,
Clock,
Target,
ListTodo,
Calendar,
Plus,
Sparkles,
} from "lucide-react";
import {
PageTemplate,
PageSection,
PageCard,
PageGrid,
} from "@/components/layout/page-template";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const BUILD_PLAN_NAV_ITEMS = [
{ title: "MVP Scope", icon: Target, href: "/build-plan" },
{ title: "Backlog", icon: ListTodo, href: "/build-plan#backlog" },
{ title: "Milestones", icon: Calendar, href: "/build-plan#milestones" },
{ title: "Progress", icon: Clock, href: "/build-plan#progress" },
];
const SAMPLE_MVP_FEATURES = [
{ id: 1, title: "User Authentication", status: "completed", priority: "high" },
{ id: 2, title: "Dashboard UI", status: "in_progress", priority: "high" },
{ id: 3, title: "Core Feature Flow", status: "in_progress", priority: "high" },
{ id: 4, title: "Payment Integration", status: "todo", priority: "medium" },
{ id: 5, title: "Email Notifications", status: "todo", priority: "low" },
];
const SAMPLE_BACKLOG = [
{ id: 1, title: "Advanced Analytics", priority: "medium" },
{ id: 2, title: "Team Collaboration", priority: "high" },
{ id: 3, title: "API Access", priority: "low" },
{ id: 4, title: "Mobile App", priority: "medium" },
];
export default function BuildPlanPage() {
const params = useParams();
const pathname = usePathname();
const workspace = params.workspace as string;
const projectId = params.projectId as string;
const sidebarItems = BUILD_PLAN_NAV_ITEMS.map((item) => {
const fullHref = `/${workspace}/project/${projectId}${item.href}`;
return {
...item,
href: fullHref,
isActive: pathname === fullHref || pathname.startsWith(fullHref),
};
});
const completedCount = SAMPLE_MVP_FEATURES.filter((f) => f.status === "completed").length;
const totalCount = SAMPLE_MVP_FEATURES.length;
const progressPercent = Math.round((completedCount / totalCount) * 100);
return (
<PageTemplate
sidebar={{
title: "Build Plan",
description: "Track what needs to be built",
items: sidebarItems,
footer: (
<div className="space-y-1">
<p className="text-xs text-muted-foreground">
{completedCount} of {totalCount} MVP features done
</p>
<div className="h-1 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${progressPercent}%` }}
/>
</div>
</div>
),
}}
hero={{
icon: ClipboardList,
title: "Build Plan",
description: "Manage your MVP scope and track progress",
actions: [
{
label: "Generate Tasks",
onClick: () => console.log("Generate tasks with AI"),
icon: Sparkles,
},
],
}}
>
{/* Progress Overview */}
<PageSection>
<PageGrid cols={4}>
<PageCard>
<div className="text-center">
<CheckCircle2 className="h-8 w-8 text-green-600 mx-auto mb-2" />
<p className="text-3xl font-bold">{completedCount}</p>
<p className="text-sm text-muted-foreground">Completed</p>
</div>
</PageCard>
<PageCard>
<div className="text-center">
<Clock className="h-8 w-8 text-blue-600 mx-auto mb-2" />
<p className="text-3xl font-bold">
{SAMPLE_MVP_FEATURES.filter((f) => f.status === "in_progress").length}
</p>
<p className="text-sm text-muted-foreground">In Progress</p>
</div>
</PageCard>
<PageCard>
<div className="text-center">
<Circle className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-3xl font-bold">
{SAMPLE_MVP_FEATURES.filter((f) => f.status === "todo").length}
</p>
<p className="text-sm text-muted-foreground">To Do</p>
</div>
</PageCard>
<PageCard>
<div className="text-center">
<Target className="h-8 w-8 text-primary mx-auto mb-2" />
<p className="text-3xl font-bold">{progressPercent}%</p>
<p className="text-sm text-muted-foreground">Progress</p>
</div>
</PageCard>
</PageGrid>
</PageSection>
{/* MVP Scope */}
<PageSection
title="MVP Scope"
description="Features included in your minimum viable product"
headerAction={
<Button size="sm" variant="ghost">
<Plus className="h-4 w-4 mr-2" />
Add Feature
</Button>
}
>
<PageCard>
<div className="space-y-2">
{SAMPLE_MVP_FEATURES.map((feature) => (
<div
key={feature.id}
className={cn(
"flex items-center gap-3 p-3 rounded-lg border transition-all hover:border-primary/50",
feature.status === "completed" && "bg-green-50/50 dark:bg-green-950/20"
)}
>
<div className="shrink-0">
{feature.status === "completed" && (
<CheckCircle2 className="h-5 w-5 text-green-600" />
)}
{feature.status === "in_progress" && (
<Clock className="h-5 w-5 text-blue-600" />
)}
{feature.status === "todo" && (
<Circle className="h-5 w-5 text-muted-foreground" />
)}
</div>
<div className="flex-1">
<p
className={cn(
"font-medium",
feature.status === "completed" && "line-through text-muted-foreground"
)}
>
{feature.title}
</p>
</div>
<div>
<span
className={cn(
"text-xs px-2 py-1 rounded-full",
feature.priority === "high" &&
"bg-red-500/10 text-red-700 dark:text-red-400",
feature.priority === "medium" &&
"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
feature.priority === "low" &&
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
)}
>
{feature.priority}
</span>
</div>
<div>
<span
className={cn(
"text-xs px-2 py-1 rounded-full",
feature.status === "completed" &&
"bg-green-500/10 text-green-700 dark:text-green-400",
feature.status === "in_progress" &&
"bg-blue-500/10 text-blue-700 dark:text-blue-400",
feature.status === "todo" &&
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
)}
>
{feature.status === "in_progress" ? "in progress" : feature.status}
</span>
</div>
</div>
))}
</div>
</PageCard>
</PageSection>
{/* Backlog */}
<PageSection
title="Backlog"
description="Features for future iterations"
headerAction={
<Button size="sm" variant="ghost">
<Plus className="h-4 w-4 mr-2" />
Add to Backlog
</Button>
}
>
<PageCard>
<div className="space-y-2">
{SAMPLE_BACKLOG.map((item) => (
<div
key={item.id}
className="flex items-center gap-3 p-3 rounded-lg border hover:border-primary/50 transition-all"
>
<ListTodo className="h-5 w-5 text-muted-foreground shrink-0" />
<div className="flex-1">
<p className="font-medium">{item.title}</p>
</div>
<span
className={cn(
"text-xs px-2 py-1 rounded-full",
item.priority === "high" &&
"bg-red-500/10 text-red-700 dark:text-red-400",
item.priority === "medium" &&
"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
item.priority === "low" &&
"bg-gray-500/10 text-gray-700 dark:text-gray-400"
)}
>
{item.priority}
</span>
<Button size="sm" variant="ghost">
Move to MVP
</Button>
</div>
))}
</div>
</PageCard>
</PageSection>
{/* Milestones */}
<PageSection title="Milestones" description="Key dates and goals">
<PageGrid cols={3}>
<PageCard>
<div className="text-center">
<div className="h-12 w-12 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-3">
<CheckCircle2 className="h-6 w-6 text-green-600" />
</div>
<h3 className="font-semibold mb-1">Alpha Release</h3>
<p className="text-sm text-muted-foreground mb-2">Completed</p>
<p className="text-xs text-muted-foreground">Jan 15, 2025</p>
</div>
</PageCard>
<PageCard className="border-primary">
<div className="text-center">
<div className="h-12 w-12 rounded-full bg-blue-500/10 flex items-center justify-center mx-auto mb-3">
<Clock className="h-6 w-6 text-blue-600" />
</div>
<h3 className="font-semibold mb-1">Beta Launch</h3>
<p className="text-sm text-muted-foreground mb-2">In Progress</p>
<p className="text-xs text-muted-foreground">Feb 1, 2025</p>
</div>
</PageCard>
<PageCard>
<div className="text-center">
<div className="h-12 w-12 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
<Target className="h-6 w-6 text-muted-foreground" />
</div>
<h3 className="font-semibold mb-1">Public Launch</h3>
<p className="text-sm text-muted-foreground mb-2">Planned</p>
<p className="text-xs text-muted-foreground">Mar 1, 2025</p>
</div>
</PageCard>
</PageGrid>
</PageSection>
</PageTemplate>
);
}

View File

@@ -0,0 +1,768 @@
'use client';
import { use, useState, useEffect, useCallback } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Loader2, CheckCircle2, Circle, Clock, RefreshCw, Eye, Cog, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
import { CollapsibleSidebar } from '@/components/ui/collapsible-sidebar';
interface WorkItem {
id: string;
title: string;
category: string;
path: string;
status: 'built' | 'missing' | 'in_progress';
priority: string;
assigned?: string;
startDate: string | null;
endDate: string | null;
duration: number;
sessionsCount: number;
commitsCount: number;
totalActivity: number;
estimatedCost?: number;
requirements: Array<{
id: number;
text: string;
status: 'built' | 'missing' | 'in_progress';
}>;
evidence: string[];
note?: string;
}
interface TimelineData {
workItems: WorkItem[];
timeline: {
start: string;
end: string;
totalDays: number;
};
summary: {
totalWorkItems: number;
withActivity: number;
noActivity: number;
built: number;
missing: number;
};
projectCreator?: string;
}
export default function TimelinePlanPage({
params,
}: {
params: Promise<{ workspace: string; projectId: string }>;
}) {
const { projectId } = use(params);
const [data, setData] = useState<TimelineData | null>(null);
const [loading, setLoading] = useState(true);
const [regenerating, setRegenerating] = useState(false);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [viewMode, setViewMode] = useState<'touchpoints' | 'technical' | 'journey'>('touchpoints');
const [collapsedJourneySections, setCollapsedJourneySections] = useState<Set<string>>(new Set());
// Map work items to types based on path and category
const getWorkItemType = (item: WorkItem): string => {
// API endpoints are System
if (item.path.startsWith('/api/')) return 'System';
// Flows are Flow
if (item.path.startsWith('flow/')) return 'Flow';
// Auth/OAuth is System
if (item.path.includes('auth') || item.path.includes('oauth')) return 'System';
// Settings is System
if (item.path.includes('settings')) return 'System';
// Marketing/Content pages
if (item.category === 'Marketing' || item.category === 'Content') return 'Screen';
// Social
if (item.category === 'Social') return 'Screen';
// Everything else is a Screen
return 'Screen';
};
// Determine if item is a user-facing touchpoint
const isTouchpoint = (item: WorkItem): boolean => {
const path = item.path.toLowerCase();
const title = item.title.toLowerCase();
// Exclude APIs and backend systems
if (path.startsWith('/api/')) return false;
if (title.includes(' api') || title.includes('api ')) return false;
// Exclude pure auth infrastructure (OAuth endpoints)
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return false;
// Include everything else - screens, pages, social posts, blogs, invites, etc.
return true;
};
// Determine if item is technical infrastructure
const isTechnical = (item: WorkItem): boolean => {
const path = item.path.toLowerCase();
const title = item.title.toLowerCase();
// APIs and backend
if (path.startsWith('/api/')) return true;
if (title.includes(' api') || title.includes('api ')) return true;
// Auth infrastructure
if (path.includes('oauth') && !path.includes('button') && !path.includes('signin')) return true;
// System settings
if (item.category === 'Settings' && title.includes('api')) return true;
return false;
};
// Map work items to customer lifecycle journey sections
const getJourneySection = (item: WorkItem): string => {
const title = item.title.toLowerCase();
const path = item.path.toLowerCase();
// Discovery - "I just found you online via social post, blog article, advertisement"
if (path === '/' || title.includes('landing') || title.includes('marketing page')) return 'Discovery';
if (item.category === 'Social' && !path.includes('settings')) return 'Discovery';
if (item.category === 'Content' && (title.includes('blog') || title.includes('article'))) return 'Discovery';
// Research - "Checking out your marketing website - features, price, home page"
if (title.includes('marketing dashboard')) return 'Research';
if (item.category === 'Marketing' && path !== '/') return 'Research';
if (path.includes('/features') || path.includes('/pricing') || path.includes('/about')) return 'Research';
if (item.category === 'Content' && path.includes('/docs') && !title.includes('getting started')) return 'Research';
// Onboarding - "Creating an account to try the product for the first time"
if (path.includes('auth') || path.includes('oauth')) return 'Onboarding';
if (path.includes('signup') || path.includes('signin') || path.includes('login')) return 'Onboarding';
if (title.includes('authentication') && !title.includes('api')) return 'Onboarding';
// First Use - "Zero state to experiencing the magic solution"
if (title.includes('onboarding')) return 'First Use';
if (title.includes('getting started')) return 'First Use';
if (path.includes('workspace') && !path.includes('settings')) return 'First Use';
if (title.includes('creation flow') || title.includes('project creation')) return 'First Use';
if (path.includes('/projects') && path.match(/\/projects\/?$/)) return 'First Use'; // Projects list page
// Active - "I've seen the magic and come back to use it again and again"
if (path.includes('overview') || path.includes('/dashboard')) return 'Active';
if (path.includes('timeline-plan') || path.includes('audit') || path.includes('mission')) return 'Active';
if (path.includes('/api/projects') || path.includes('mvp-checklist')) return 'Active';
if (title.includes('plan generation') || title.includes('marketing plan')) return 'Active';
if (path.includes('projects/') && path.length > '/projects/'.length) return 'Active'; // Specific project pages
// Support - "I've got questions, need quick answers to get back to the magic"
if (path.includes('settings')) return 'Support';
if (path.includes('/help') || path.includes('/faq') || path.includes('/support')) return 'Support';
if (item.category === 'Content' && path.includes('/docs') && title.includes('help')) return 'Support';
// Purchase - "Time to pay so I can keep using the magic"
if (path.includes('billing') || path.includes('payment') || path.includes('subscription')) return 'Purchase';
if (path.includes('upgrade') || path.includes('checkout') || path.includes('pricing/buy')) return 'Purchase';
// Default to Active for core product features
return 'Active';
};
const toggleJourneySection = (sectionId: string) => {
setCollapsedJourneySections(prev => {
const newSet = new Set(prev);
if (newSet.has(sectionId)) {
newSet.delete(sectionId);
} else {
newSet.add(sectionId);
}
return newSet;
});
};
// Get emoji icon for journey section
const getJourneySectionIcon = (section: string): string => {
const icons: Record<string, string> = {
'Discovery': '🔍',
'Research': '📚',
'Onboarding': '🎯',
'First Use': '🚀',
'Active': '⚡',
'Support': '💡',
'Purchase': '💳'
};
return icons[section] || '📋';
};
// Get phase status based on overall item status
const getPhaseStatus = (itemStatus: string, phase: 'scope' | 'design' | 'code'): 'built' | 'in_progress' | 'missing' => {
if (itemStatus === 'built') return 'built';
if (itemStatus === 'missing') return 'missing';
// If in_progress, show progression through phases
if (phase === 'scope') return 'built';
if (phase === 'design') return 'in_progress';
return 'missing';
};
// Render status badge
const renderStatusBadge = (status: 'built' | 'in_progress' | 'missing') => {
if (status === 'built') {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-100 text-green-800 text-xs font-medium">
<CheckCircle2 className="h-3 w-3" />
Done
</span>
);
}
if (status === 'in_progress') {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-blue-100 text-blue-800 text-xs font-medium">
<Clock className="h-3 w-3" />
Started
</span>
);
}
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-gray-100 text-gray-800 text-xs font-medium">
<Circle className="h-3 w-3" />
To-do
</span>
);
};
const loadTimelineData = useCallback(async () => {
try {
setLoading(true);
const response = await fetch(`/api/projects/${projectId}/timeline-view`);
const result = await response.json();
// Check if the response is an error
if (result.error) {
console.error('API Error:', result.error, result.details);
alert(`Failed to load timeline: ${result.details || result.error}`);
return;
}
setData(result);
} catch (error) {
console.error('Error loading timeline:', error);
alert('Failed to load timeline data. Check console for details.');
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
loadTimelineData();
}, [loadTimelineData]);
const regeneratePlan = async () => {
if (!confirm('Regenerate the plan? This will analyze your project and create a fresh MVP checklist.')) {
return;
}
try {
setRegenerating(true);
const response = await fetch(`/api/projects/${projectId}/mvp-checklist`, {
method: 'POST',
});
if (!response.ok) {
throw new Error('Failed to regenerate plan');
}
// Reload the timeline data
await loadTimelineData();
} catch (error) {
console.error('Error regenerating plan:', error);
alert('Failed to regenerate plan. Check console for details.');
} finally {
setRegenerating(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (!data) {
return <div className="p-8 text-center text-muted-foreground">No timeline data available</div>;
}
return (
<div className="relative h-full w-full bg-background overflow-hidden flex">
{/* Left Sidebar */}
<CollapsibleSidebar>
<div className="space-y-4">
<div>
<h3 className="text-sm font-semibold mb-2">Quick Stats</h3>
<div className="space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Total Items</span>
<span className="font-medium">{data.summary.totalWorkItems}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Built</span>
<span className="font-medium text-green-600">{data.summary.built}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">In Progress</span>
<span className="font-medium text-blue-600">{data.summary.withActivity - data.summary.built}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">To Build</span>
<span className="font-medium text-gray-600">{data.summary.missing}</span>
</div>
</div>
</div>
</div>
</CollapsibleSidebar>
{/* Main Content */}
<div className="flex-1 flex flex-col p-4 space-y-3 overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold">MVP Checklist</h1>
<p className="text-sm text-muted-foreground mt-0.5">
{data.summary.built} of {data.summary.totalWorkItems} pages built
{data.summary.withActivity} with development activity
</p>
</div>
<div className="flex gap-3 items-center">
{/* View Mode Switcher */}
<div className="flex items-center border rounded-lg p-1">
<Button
variant={viewMode === 'touchpoints' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('touchpoints')}
className="gap-2 h-7"
>
<Eye className="h-4 w-4" />
Touchpoints
</Button>
<Button
variant={viewMode === 'technical' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('technical')}
className="gap-2 h-7"
>
<Cog className="h-4 w-4" />
Technical
</Button>
<Button
variant={viewMode === 'journey' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setViewMode('journey')}
className="gap-2 h-7"
>
<GitBranch className="h-4 w-4" />
Journey
</Button>
</div>
{/* Regenerate Button */}
<Button
variant="outline"
size="sm"
onClick={regeneratePlan}
disabled={regenerating}
className="gap-2"
>
{regenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Regenerating...
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
Regenerate Plan
</>
)}
</Button>
{/* Summary Stats */}
<div className="text-xs px-3 py-1 bg-green-100 text-green-800 rounded">
{data.summary.built} Built
</div>
<div className="text-xs px-3 py-1 bg-gray-100 text-gray-800 rounded">
{data.summary.missing} To Build
</div>
</div>
</div>
{/* Touchpoints View - What users see and engage with */}
{viewMode === 'touchpoints' && (
<Card className="flex-1 overflow-hidden flex flex-col p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Everything users see and engage with - screens, features, social posts, blogs, invites, and all customer-facing elements.</p>
</div>
<div className="overflow-auto">
<table className="w-full">
<thead className="bg-muted/50 border-b sticky top-0">
<tr>
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Touchpoint</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
</tr>
</thead>
<tbody className="divide-y">
{data?.workItems.filter(item => isTouchpoint(item)).map((item, index) => (
<tr
key={item.id}
className="hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<td className="px-4 py-3 text-sm text-muted-foreground">
{getWorkItemType(item)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{item.status === 'built' ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : item.status === 'in_progress' ? (
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
)}
<div>
<div className="text-sm font-medium">{item.title}</div>
{expandedItems.has(item.id) && (
<div className="mt-2 space-y-1">
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</td>
<td className="px-4 py-3">
<span className="text-sm text-muted-foreground">
{item.assigned || data?.projectCreator || 'You'}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.sessionsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.commitsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
{/* Journey View - Customer lifecycle stages */}
{viewMode === 'journey' && (
<Card className="flex-1 overflow-auto p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Customer lifecycle journey from discovery to purchase - organizing all touchpoints and technical components by user stage.</p>
</div>
<div className="divide-y">
{/* Journey Sections - Customer Lifecycle */}
{['Discovery', 'Research', 'Onboarding', 'First Use', 'Active', 'Support', 'Purchase'].map(sectionName => {
const sectionItems = data.workItems.filter(item => getJourneySection(item) === sectionName);
if (sectionItems.length === 0) return null;
const sectionStats = {
done: sectionItems.filter(i => i.status === 'built').length,
started: sectionItems.filter(i => i.status === 'in_progress').length,
todo: sectionItems.filter(i => i.status === 'missing').length,
total: sectionItems.length
};
const isCollapsed = collapsedJourneySections.has(sectionName);
return (
<div key={sectionName}>
{/* Section Header */}
<div
className="bg-muted/30 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-between sticky top-0 z-10"
onClick={() => toggleJourneySection(sectionName)}
>
<div className="flex items-center gap-3">
{isCollapsed ? (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-lg">{getJourneySectionIcon(sectionName)}</span>
<h3 className="font-semibold text-base">{sectionName}</h3>
<span className="text-xs text-muted-foreground">
{sectionStats.done}/{sectionStats.total} complete
</span>
</div>
<div className="flex gap-2 text-xs">
{sectionStats.done > 0 && (
<span className="px-2 py-1 bg-green-100 text-green-800 rounded">
{sectionStats.done} done
</span>
)}
{sectionStats.started > 0 && (
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded">
{sectionStats.started} started
</span>
)}
{sectionStats.todo > 0 && (
<span className="px-2 py-1 bg-gray-100 text-gray-800 rounded">
{sectionStats.todo} to-do
</span>
)}
</div>
</div>
{/* Section Items */}
{!isCollapsed && (
<div className="divide-y">
{sectionItems.map(item => (
<div key={item.id} className="px-4 py-3 hover:bg-accent/30 transition-colors">
<div
className="flex items-start justify-between cursor-pointer"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<div className="flex items-start gap-3 flex-1">
{/* Status Icon */}
{item.status === 'built' ? (
<CheckCircle2 className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
) : item.status === 'in_progress' ? (
<Clock className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
) : (
<Circle className="h-5 w-5 text-gray-400 flex-shrink-0 mt-0.5" />
)}
<div className="flex-1">
{/* Title and Type */}
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{item.title}</span>
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">
{getWorkItemType(item)}
</span>
</div>
{/* Phase Status */}
<div className="flex gap-2 mt-2">
<div className="text-xs">
<span className="text-muted-foreground">Spec:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</div>
<div className="text-xs">
<span className="text-muted-foreground">Design:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</div>
<div className="text-xs">
<span className="text-muted-foreground">Code:</span>{' '}
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</div>
</div>
{/* Expanded Requirements */}
{expandedItems.has(item.id) && (
<div className="mt-3 space-y-1 pl-4 border-l-2 border-gray-200">
<p className="text-xs font-semibold text-muted-foreground mb-2">Requirements:</p>
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Right Side Stats */}
<div className="flex items-start gap-4 text-xs text-muted-foreground">
<div className="text-center">
<div className="font-medium">Sessions</div>
<div className={item.sessionsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.sessionsCount}</div>
</div>
<div className="text-center">
<div className="font-medium">Commits</div>
<div className={item.commitsCount > 0 ? 'text-blue-600 font-bold' : ''}>{item.commitsCount}</div>
</div>
<div className="text-center min-w-[60px]">
<div className="font-medium">Cost</div>
<div>{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</Card>
)}
{/* Technical View - Infrastructure that powers everything */}
{viewMode === 'technical' && (
<Card className="flex-1 overflow-hidden flex flex-col p-0">
<div className="p-4 border-b bg-muted/30">
<p className="text-sm text-muted-foreground">Technical infrastructure that powers the product - APIs, backend services, authentication, and system integrations.</p>
</div>
<div className="overflow-auto">
<table className="w-full">
<thead className="bg-muted/50 border-b sticky top-0">
<tr>
<th className="text-left px-4 py-3 text-sm font-semibold">Type</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Technical Component</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Scope</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Design</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Code</th>
<th className="text-left px-4 py-3 text-sm font-semibold">Assigned</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Sessions</th>
<th className="text-center px-4 py-3 text-sm font-semibold">Commits</th>
<th className="text-right px-4 py-3 text-sm font-semibold">Cost</th>
</tr>
</thead>
<tbody className="divide-y">
{data?.workItems.filter(item => isTechnical(item)).map((item, index) => (
<tr
key={item.id}
className="hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => {
setExpandedItems(prev => {
const newSet = new Set(prev);
if (newSet.has(item.id)) {
newSet.delete(item.id);
} else {
newSet.add(item.id);
}
return newSet;
});
}}
>
<td className="px-4 py-3 text-sm text-muted-foreground">
{getWorkItemType(item)}
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2">
{item.status === 'built' ? (
<CheckCircle2 className="h-4 w-4 text-green-600 flex-shrink-0" />
) : item.status === 'in_progress' ? (
<Clock className="h-4 w-4 text-blue-600 flex-shrink-0" />
) : (
<Circle className="h-4 w-4 text-gray-400 flex-shrink-0" />
)}
<div>
<div className="text-sm font-medium">{item.title}</div>
{expandedItems.has(item.id) && (
<div className="mt-2 space-y-1">
{item.requirements.map((req) => (
<div key={req.id} className="flex items-center gap-2 text-xs text-muted-foreground ml-6">
{req.status === 'built' ? (
<CheckCircle2 className="h-3 w-3 text-green-600" />
) : (
<Circle className="h-3 w-3 text-gray-400" />
)}
<span>{req.text}</span>
</div>
))}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'scope'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'design'))}
</td>
<td className="px-4 py-3">
{renderStatusBadge(getPhaseStatus(item.status, 'code'))}
</td>
<td className="px-4 py-3">
<span className="text-sm text-muted-foreground">
{item.assigned || data?.projectCreator || 'You'}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.sessionsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.sessionsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-center">
<span className={item.commitsCount > 0 ? 'text-blue-600 font-medium' : 'text-gray-400'}>
{item.commitsCount}
</span>
</td>
<td className="px-4 py-3 text-sm text-right text-muted-foreground">
{item.estimatedCost ? `$${item.estimatedCost.toFixed(2)}` : '-'}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
{/* End Main Content */}
</div>
);
}