477 lines
18 KiB
TypeScript
477 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Activity, Clock, DollarSign, FolderOpen, Settings, Loader2, Github, MessageSquare, CheckCircle2, AlertCircle, Link as LinkIcon } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useParams } from "next/navigation";
|
|
import { db, auth } from "@/lib/firebase/config";
|
|
import { doc, getDoc, collection, query, where, getDocs } from "firebase/firestore";
|
|
import { toast } from "sonner";
|
|
|
|
interface Project {
|
|
id: string;
|
|
name: string;
|
|
productName: string;
|
|
productVision?: string;
|
|
workspacePath?: string;
|
|
workspaceName?: string;
|
|
githubRepo?: string;
|
|
githubRepoUrl?: string;
|
|
chatgptUrl?: string;
|
|
projectType: 'scratch' | 'existing';
|
|
status: string;
|
|
createdAt: any;
|
|
}
|
|
|
|
interface Stats {
|
|
totalSessions: number;
|
|
totalCost: number;
|
|
totalTokens: number;
|
|
totalDuration: number;
|
|
}
|
|
|
|
interface UnassociatedSession {
|
|
id: string;
|
|
workspacePath: string;
|
|
workspaceName: string;
|
|
count: number;
|
|
}
|
|
|
|
export default function ProjectOverviewPage() {
|
|
const params = useParams();
|
|
const projectId = params.projectId as string;
|
|
const workspace = params.workspace as string;
|
|
|
|
const [project, setProject] = useState<Project | null>(null);
|
|
const [stats, setStats] = useState<Stats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [unassociatedSessions, setUnassociatedSessions] = useState<number>(0);
|
|
const [linkingSessions, setLinkingSessions] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const fetchProjectData = async () => {
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) {
|
|
setError('Not authenticated');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Fetch project details
|
|
const projectDoc = await getDoc(doc(db, 'projects', projectId));
|
|
if (!projectDoc.exists()) {
|
|
setError('Project not found');
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const projectData = projectDoc.data() as Project;
|
|
setProject({ ...projectData, id: projectDoc.id });
|
|
|
|
// Fetch stats
|
|
const statsResponse = await fetch(`/api/stats?projectId=${projectId}`);
|
|
if (statsResponse.ok) {
|
|
const statsData = await statsResponse.json();
|
|
setStats(statsData);
|
|
}
|
|
|
|
// If project has a workspace path, check for unassociated sessions
|
|
if (projectData.workspacePath) {
|
|
try {
|
|
const sessionsRef = collection(db, 'sessions');
|
|
const unassociatedQuery = query(
|
|
sessionsRef,
|
|
where('userId', '==', user.uid),
|
|
where('workspacePath', '==', projectData.workspacePath),
|
|
where('needsProjectAssociation', '==', true)
|
|
);
|
|
const unassociatedSnap = await getDocs(unassociatedQuery);
|
|
setUnassociatedSessions(unassociatedSnap.size);
|
|
} catch (err) {
|
|
// Index might not be ready yet, silently fail
|
|
console.log('Could not check for unassociated sessions:', err);
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error fetching project:', err);
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const unsubscribe = auth.onAuthStateChanged((user) => {
|
|
if (user) {
|
|
fetchProjectData();
|
|
} else {
|
|
setError('Not authenticated');
|
|
setLoading(false);
|
|
}
|
|
});
|
|
|
|
return () => unsubscribe();
|
|
}, [projectId]);
|
|
|
|
const handleLinkSessions = async () => {
|
|
if (!project?.workspacePath) return;
|
|
|
|
setLinkingSessions(true);
|
|
try {
|
|
const user = auth.currentUser;
|
|
if (!user) {
|
|
toast.error('You must be signed in');
|
|
return;
|
|
}
|
|
|
|
const token = await user.getIdToken();
|
|
|
|
const response = await fetch('/api/sessions/associate-project', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
projectId,
|
|
workspacePath: project.workspacePath,
|
|
workspaceName: project.workspaceName,
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
toast.success(`✅ Linked ${unassociatedSessions} sessions to this project!`);
|
|
setUnassociatedSessions(0);
|
|
|
|
// Refresh stats
|
|
const statsResponse = await fetch(`/api/stats?projectId=${projectId}`);
|
|
if (statsResponse.ok) {
|
|
const statsData = await statsResponse.json();
|
|
setStats(statsData);
|
|
}
|
|
} else {
|
|
toast.error('Failed to link sessions');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error linking sessions:', error);
|
|
toast.error('An error occurred');
|
|
} finally {
|
|
setLinkingSessions(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error || !project) {
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center gap-4">
|
|
<div className="rounded-full bg-red-500/10 p-6">
|
|
<Activity className="h-12 w-12 text-red-500" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold">Error Loading Project</h2>
|
|
<p className="text-muted-foreground">{error || 'Project not found'}</p>
|
|
<Link href={`/${workspace}/projects`}>
|
|
<Button>Back to Projects</Button>
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-auto">
|
|
{/* Header */}
|
|
<div className="border-b px-6 py-4">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="text-4xl">📦</div>
|
|
<div>
|
|
<h1 className="text-3xl font-bold">{project.productName}</h1>
|
|
<p className="text-sm text-muted-foreground">{project.name}</p>
|
|
</div>
|
|
</div>
|
|
{project.productVision && (
|
|
<p className="text-muted-foreground mt-2 max-w-2xl">{project.productVision}</p>
|
|
)}
|
|
{project.workspacePath && (
|
|
<div className="flex items-center gap-2 mt-3 text-sm text-muted-foreground">
|
|
<FolderOpen className="h-4 w-4" />
|
|
<code className="font-mono">{project.workspacePath}</code>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<Link href={`/${workspace}/project/${projectId}/settings`}>
|
|
<Button variant="outline" size="sm">
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Settings
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-auto p-6">
|
|
<div className="mx-auto max-w-6xl space-y-6">
|
|
|
|
{/* 🔗 Link Unassociated Sessions - Show this FIRST if available */}
|
|
{unassociatedSessions > 0 && (
|
|
<Card className="border-blue-500/50 bg-blue-50/50 dark:bg-blue-950/20">
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<LinkIcon className="h-5 w-5 text-blue-600" />
|
|
Sessions Detected!
|
|
</CardTitle>
|
|
<CardDescription className="mt-2">
|
|
We found <strong>{unassociatedSessions} coding session{unassociatedSessions > 1 ? 's' : ''}</strong> from this workspace that {unassociatedSessions > 1 ? 'aren\'t' : 'isn\'t'} linked to any project yet.
|
|
</CardDescription>
|
|
</div>
|
|
<Button
|
|
onClick={handleLinkSessions}
|
|
disabled={linkingSessions}
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
{linkingSessions ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Linking...
|
|
</>
|
|
) : (
|
|
<>
|
|
<LinkIcon className="mr-2 h-4 w-4" />
|
|
Link {unassociatedSessions} Session{unassociatedSessions > 1 ? 's' : ''}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="rounded-lg bg-white dark:bg-gray-900 p-4">
|
|
<p className="text-sm text-muted-foreground">
|
|
<strong>Workspace:</strong> <code className="font-mono text-xs">{project.workspacePath}</code>
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
Linking these sessions will add their costs, time, and activity to this project's stats.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Stats Cards - Only show if there are sessions */}
|
|
{stats && stats.totalSessions > 0 && (
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<Activity className="h-4 w-4" />
|
|
Total Sessions
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{stats.totalSessions}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<Clock className="h-4 w-4" />
|
|
Total Time
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{stats.totalDuration}m</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<DollarSign className="h-4 w-4" />
|
|
Total Cost
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">${stats.totalCost.toFixed(2)}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
|
<Activity className="h-4 w-4" />
|
|
Tokens Used
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold">{stats.totalTokens.toLocaleString()}</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Quick Actions */}
|
|
<div className="grid gap-4 md:grid-cols-3">
|
|
<Link href={`/${workspace}/project/${projectId}/sessions`}>
|
|
<Card className="hover:border-primary transition-all cursor-pointer h-full">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Activity className="h-5 w-5" />
|
|
Sessions
|
|
</CardTitle>
|
|
<CardDescription>View all coding sessions</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href={`/${workspace}/project/${projectId}/analytics`}>
|
|
<Card className="hover:border-primary transition-all cursor-pointer h-full">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<DollarSign className="h-5 w-5" />
|
|
Analytics
|
|
</CardTitle>
|
|
<CardDescription>Cost & usage analytics</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
</Link>
|
|
|
|
<Link href={`/${workspace}/connections`}>
|
|
<Card className="hover:border-primary transition-all cursor-pointer h-full">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Settings className="h-5 w-5" />
|
|
Connections
|
|
</CardTitle>
|
|
<CardDescription>Manage integrations</CardDescription>
|
|
</CardHeader>
|
|
</Card>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Getting Started - Show if no sessions and no unassociated */}
|
|
{stats && stats.totalSessions === 0 && unassociatedSessions === 0 && (
|
|
<Card className="border-dashed">
|
|
<CardContent className="py-12">
|
|
<div className="text-center space-y-4">
|
|
{/* Show different icons based on project type */}
|
|
<div className="rounded-full bg-primary/10 p-6 mx-auto w-fit">
|
|
{project.workspacePath && <FolderOpen className="h-12 w-12 text-primary" />}
|
|
{project.githubRepo && <Github className="h-12 w-12 text-primary" />}
|
|
{project.chatgptUrl && <MessageSquare className="h-12 w-12 text-primary" />}
|
|
{!project.workspacePath && !project.githubRepo && !project.chatgptUrl && (
|
|
<Activity className="h-12 w-12 text-primary" />
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
{/* Dynamic title based on project type */}
|
|
{project.workspacePath && (
|
|
<>
|
|
<h3 className="text-2xl font-bold mb-2">Start Coding in This Workspace!</h3>
|
|
<p className="text-muted-foreground max-w-md mx-auto">
|
|
Open <code className="bg-muted px-2 py-1 rounded">{project.workspacePath}</code> in Cursor and start coding.
|
|
Sessions from this workspace will automatically appear here.
|
|
</p>
|
|
</>
|
|
)}
|
|
|
|
{project.githubRepo && (
|
|
<>
|
|
<h3 className="text-2xl font-bold mb-2">Clone & Start Coding!</h3>
|
|
<p className="text-muted-foreground max-w-md mx-auto">
|
|
Clone your repository and open it in Cursor to start tracking your development.
|
|
</p>
|
|
<div className="mt-4">
|
|
<a
|
|
href={project.githubRepoUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 text-sm text-blue-600 hover:underline"
|
|
>
|
|
<Github className="h-4 w-4" />
|
|
View on GitHub →
|
|
</a>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{project.chatgptUrl && (
|
|
<>
|
|
<h3 className="text-2xl font-bold mb-2">Turn Your Idea Into Code!</h3>
|
|
<p className="text-muted-foreground max-w-md mx-auto">
|
|
Start building based on your ChatGPT conversation. Open your project in Cursor to begin tracking.
|
|
</p>
|
|
<div className="mt-4">
|
|
<a
|
|
href={project.chatgptUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 text-sm text-green-600 hover:underline"
|
|
>
|
|
<MessageSquare className="h-4 w-4" />
|
|
View ChatGPT Conversation →
|
|
</a>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{!project.workspacePath && !project.githubRepo && !project.chatgptUrl && (
|
|
<>
|
|
<h3 className="text-2xl font-bold mb-2">Start Building!</h3>
|
|
<p className="text-muted-foreground max-w-md mx-auto">
|
|
Create your project directory and open it in Cursor to start tracking your development.
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-4 justify-center pt-4">
|
|
<Link href={`/${workspace}/connections`}>
|
|
<Button size="lg">
|
|
Setup Cursor Extension
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Setup status */}
|
|
<div className="mt-8 pt-6 border-t">
|
|
<h4 className="text-sm font-medium mb-3">Setup Checklist</h4>
|
|
<div className="flex flex-col gap-2 max-w-md mx-auto text-left">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
<span>Project created</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<span>Install Cursor Monitor extension</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<span>Start coding in your workspace</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|