diff --git a/app/[workspace]/projects/page.tsx b/app/[workspace]/projects/page.tsx index c7b7075..2a4a745 100644 --- a/app/[workspace]/projects/page.tsx +++ b/app/[workspace]/projects/page.tsx @@ -1,7 +1,7 @@ "use client"; -// Force rebuild - v3 import { useEffect, useState } from "react"; +import { useSession } from "next-auth/react"; import { Card, CardContent, @@ -13,8 +13,6 @@ import { Button } from "@/components/ui/button"; import { Plus, Sparkles, Loader2, MoreVertical, Trash2 } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; -import { db, auth } from "@/lib/firebase/config"; -import { collection, query, where, orderBy, getDocs } from "firebase/firestore"; import { ProjectCreationModal } from "@/components/project-creation-modal"; import { DropdownMenu, @@ -42,18 +40,34 @@ interface ProjectWithStats { productVision?: string; workspacePath?: string; status?: string; - createdAt: { toDate?: () => Date } | Date | string | number | null; - updatedAt: { toDate?: () => Date } | Date | string | number | null; + createdAt: string | null; + updatedAt: string | null; stats: { sessions: number; costs: number; }; } +function getTimeAgo(dateStr: string | null | undefined): string { + if (!dateStr) return "Unknown"; + const date = new Date(dateStr); + if (isNaN(date.getTime())) return "Unknown"; + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + if (days === 0) return "Today"; + if (days === 1) return "Yesterday"; + if (days < 7) return `${days} days ago`; + if (days < 30) return `${Math.floor(days / 7)} weeks ago`; + if (days < 365) return `${Math.floor(days / 30)} months ago`; + return `${Math.floor(days / 365)} years ago`; +} + export default function ProjectsPage() { const params = useParams(); const workspace = params.workspace as string; - + const { data: session, status } = useSession(); + const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -61,175 +75,90 @@ export default function ProjectsPage() { const [projectToDelete, setProjectToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); - const fetchProjects = async (user: { uid: string }) => { + const fetchProjects = async () => { try { - // Fetch projects for this user - const projectsRef = collection(db, 'projects'); - const projectsQuery = query( - projectsRef, - where('userId', '==', user.uid), - orderBy('createdAt', 'desc') - ); - const projectsSnapshot = await getDocs(projectsQuery); - - // Fetch sessions to calculate stats - const sessionsRef = collection(db, 'sessions'); - const sessionsQuery = query( - sessionsRef, - where('userId', '==', user.uid) - ); - const sessionsSnapshot = await getDocs(sessionsQuery); - - // Group sessions by project - const sessionsByProject = new Map(); - sessionsSnapshot.docs.forEach(doc => { - const session = doc.data(); - const projectId = session.projectId || 'unassigned'; - const existing = sessionsByProject.get(projectId) || { count: 0, totalCost: 0 }; - sessionsByProject.set(projectId, { - count: existing.count + 1, - totalCost: existing.totalCost + (session.cost || 0), - }); - }); - - // Map projects with stats - const projectsWithStats: ProjectWithStats[] = projectsSnapshot.docs.map(doc => { - const project = doc.data(); - const stats = sessionsByProject.get(doc.id) || { count: 0, totalCost: 0 }; - - return { - id: doc.id, - name: project.name, - slug: project.slug, - productName: project.productName, - productVision: project.productVision, - workspacePath: project.workspacePath, - status: project.status || 'active', - createdAt: project.createdAt, - updatedAt: project.updatedAt, - stats: { - sessions: stats.count, - costs: stats.totalCost, - }, - }; - }); - - setProjects(projectsWithStats); + setLoading(true); + const res = await fetch("/api/projects"); + if (!res.ok) { + const err = await res.json(); + throw new Error(err.error || "Failed to fetch projects"); + } + const data = await res.json(); + setProjects(data.projects || []); + setError(null); } catch (err: unknown) { - console.error('Error fetching projects:', err); - setError(err instanceof Error ? err.message : 'Unknown error'); + console.error("Error fetching projects:", err); + setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); } }; useEffect(() => { - const unsubscribe = auth.onAuthStateChanged(async (user) => { - if (!user) { - setError('Not authenticated'); - setLoading(false); - return; - } - - fetchProjects(user); - }); - - return () => unsubscribe(); - }, []); + if (status === "authenticated") { + fetchProjects(); + } else if (status === "unauthenticated") { + setLoading(false); + } + }, [status]); const handleDeleteProject = async () => { if (!projectToDelete) return; - setIsDeleting(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/projects/delete', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - projectId: projectToDelete.id, - }), + const res = await fetch("/api/projects/delete", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ projectId: projectToDelete.id }), }); - if (response.ok) { - const data = await response.json(); - toast.success('Project deleted successfully', { - description: data.sessionsPreserved > 0 - ? `${data.sessionsPreserved} sessions preserved and can be reassigned` - : undefined - }); - - // Remove from local state - setProjects(projects.filter(p => p.id !== projectToDelete.id)); + if (res.ok) { + toast.success("Project deleted"); setProjectToDelete(null); + fetchProjects(); } else { - const error = await response.json(); - toast.error(error.error || 'Failed to delete project'); + const err = await res.json(); + toast.error(err.error || "Failed to delete project"); } - } catch (error) { - console.error('Error deleting project:', error); - toast.error('An error occurred while deleting'); + } catch (err) { + console.error("Delete error:", err); + toast.error("An error occurred"); } finally { setIsDeleting(false); } }; - const getTimeAgo = (timestamp: { toDate?: () => Date } | Date | string | number | null | undefined) => { - if (!timestamp) return 'Recently'; - - let date: Date; - if (typeof timestamp === 'object' && timestamp !== null && 'toDate' in timestamp && timestamp.toDate) { - date = timestamp.toDate(); - } else { - date = new Date(timestamp as string | number | Date); - } - - const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000); - - if (seconds < 60) return 'Just now'; - if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`; - if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`; - if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days ago`; - return date.toLocaleDateString(); - }; - + if (status === "loading") { + return ( +
+ +
+ ); + } + return ( <> - {/* Header */} -
-
-

Projects

-

- Manage your product development projects -

+
+
+
+

Projects

+

+ {session?.user?.email} +

+
+
- -
- {/* Content */} -
-
- {/* Loading State */} +
{loading && (
)} - {/* Error State */} {error && ( @@ -238,91 +167,85 @@ export default function ProjectsPage() { )} - {/* Projects Grid */} {!loading && !error && projects.length > 0 && (

Your Projects

-
- {projects.map((project) => { - const projectHref = `/${workspace}/project/${project.id}/overview`; - return ( -
- - - -
-
-
📦
-
- {project.productName} - - {getTimeAgo(project.updatedAt)} - -
-
-
-
- {project.status} -
- - e.preventDefault()}> - - - - { - e.preventDefault(); - setProjectToDelete(project); - }} - > - - Delete Project - - - -
-
-
- -

- {project.productVision || 'No description'} -

- {project.workspacePath && ( -

- 📁 {project.workspacePath.split('/').pop()} -

- )} -
-
- Sessions:{" "} - {project.stats.sessions} -
-
- Costs:{" "} - ${project.stats.costs.toFixed(2)} -
-
-
-
- -
- ); - })} +
+ {projects.map((project) => { + const projectHref = `/${workspace}/project/${project.id}/overview`; + return ( +
+ + + +
+
+
📦
+
+ {project.productName} + + {getTimeAgo(project.updatedAt)} + +
+
+
+
+ {project.status} +
+ + e.preventDefault()}> + + + + { + e.preventDefault(); + setProjectToDelete(project); + }} + > + + Delete Project + + + +
+
+
+ +

+ {project.productVision || "No description"} +

+ {project.workspacePath && ( +

+ 📁 {project.workspacePath.split("/").pop()} +

+ )} +
+
+ Sessions:{" "} + {project.stats.sessions} +
+
+ Costs:{" "} + ${project.stats.costs.toFixed(2)} +
+
+
+
+ +
+ ); + })} - {/* New Project Card */} setShowCreationModal(true)} @@ -341,7 +264,6 @@ export default function ProjectsPage() {
)} - {/* Empty State (show when no projects) */} {!loading && !error && projects.length === 0 && ( @@ -350,8 +272,7 @@ export default function ProjectsPage() {

No projects yet

- Create your first project to start tracking your AI-powered development - workflow, manage costs, and maintain living documentation. + Create your first project to start tracking your AI-powered development workflow.

- {/* Project Creation Modal */} { setShowCreationModal(open); - if (!open) { - // Refresh projects list when modal closes - const user = auth.currentUser; - if (user) fetchProjects(user); - } + if (!open) fetchProjects(); }} workspace={workspace} /> - {/* Delete Project Confirmation Dialog */} !open && setProjectToDelete(null)}> Are you absolutely sure? - This action will delete the project "{projectToDelete?.productName}". - All associated sessions and data will be preserved but unlinked from this project. - You can reassign them to another project later. + This will delete "{projectToDelete?.productName}". Sessions will be preserved but unlinked. Cancel - {isDeleting ? ( - - ) : ( - - )} + {isDeleting ? : } Delete Project @@ -404,4 +313,3 @@ export default function ProjectsPage() { ); } -