Files
vibn-frontend/components/project-association-prompt.tsx

308 lines
9.9 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { db, auth } from '@/lib/firebase/config';
import { collection, query, where, limit, getDocs, orderBy } from 'firebase/firestore';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { FolderOpen, Plus, Link as LinkIcon } from 'lucide-react';
import { toast } from 'sonner';
import { ProjectCreationModal } from './project-creation-modal';
interface UnassociatedWorkspace {
workspacePath: string;
workspaceName: string;
sessionCount: number;
}
interface Project {
id: string;
name: string;
productName: string;
slug: string;
}
export function ProjectAssociationPrompt({ workspace }: { workspace: string }) {
// Temporarily disabled - will be re-enabled with better UX
return null;
const [unassociatedWorkspace, setUnassociatedWorkspace] = useState<UnassociatedWorkspace | null>(null);
const [projects, setProjects] = useState<Project[]>([]);
const [showDialog, setShowDialog] = useState(false);
const [showCreationModal, setShowCreationModal] = useState(false);
const [loading, setLoading] = useState(false);
const [dismissedWorkspaces, setDismissedWorkspaces] = useState<Set<string>>(new Set());
const [hasCheckedThisSession, setHasCheckedThisSession] = useState(false);
// Load dismissed workspaces from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem('dismissedWorkspaces');
if (stored) {
try {
setDismissedWorkspaces(new Set(JSON.parse(stored)));
} catch (e) {
console.error('Error loading dismissed workspaces:', e);
}
}
}, []);
useEffect(() => {
let unsubscribe: () => void;
const checkForUnassociatedSessions = async (user: any) => {
// Check if we've already shown the prompt in this browser session
const lastPromptTime = sessionStorage.getItem('vibn_last_workspace_prompt');
const now = Date.now();
const fiveMinutes = 5 * 60 * 1000;
if (lastPromptTime && (now - parseInt(lastPromptTime)) < fiveMinutes) {
console.log('⏭️ Already checked recently, skipping');
return;
}
try {
// Mark that we've checked
sessionStorage.setItem('vibn_last_workspace_prompt', now.toString());
// Check for sessions that need project association
const sessionsRef = collection(db, 'sessions');
const q = query(
sessionsRef,
where('userId', '==', user.uid),
where('needsProjectAssociation', '==', true),
orderBy('createdAt', 'desc'),
limit(1)
);
const snapshot = await getDocs(q);
if (!snapshot.empty) {
const session = snapshot.docs[0].data();
// Check if this workspace was dismissed
if (dismissedWorkspaces.has(session.workspacePath)) {
console.log('⏭️ Workspace was dismissed, skipping prompt');
return;
}
// Count sessions from this workspace
const countQuery = query(
sessionsRef,
where('userId', '==', user.uid),
where('workspacePath', '==', session.workspacePath),
where('needsProjectAssociation', '==', true)
);
const countSnapshot = await getDocs(countQuery);
setUnassociatedWorkspace({
workspacePath: session.workspacePath,
workspaceName: session.workspaceName || 'Unknown',
sessionCount: countSnapshot.size,
});
// Fetch user's projects for linking
const projectsRef = collection(db, 'projects');
const projectsQuery = query(
projectsRef,
where('userId', '==', user.uid),
orderBy('createdAt', 'desc')
);
const projectsSnapshot = await getDocs(projectsQuery);
const userProjects = projectsSnapshot.docs.map(doc => ({
id: doc.id,
name: doc.data().name,
productName: doc.data().productName,
slug: doc.data().slug,
}));
setProjects(userProjects);
setShowDialog(true);
}
} catch (error: any) {
// Silently handle index building errors - the feature will work once indexes are ready
if (error?.message?.includes('index')) {
console.log('⏳ Firestore indexes are still building. Project detection will be available shortly.');
} else {
console.error('Error checking for unassociated sessions:', error);
}
}
};
unsubscribe = auth.onAuthStateChanged((user) => {
if (user) {
checkForUnassociatedSessions(user);
}
});
return () => {
if (unsubscribe) unsubscribe();
};
}, []); // Empty dependency array - only run once on mount
const handleCreateNewProject = () => {
setShowDialog(false);
setShowCreationModal(true);
};
const handleRemindLater = () => {
if (!unassociatedWorkspace) return;
// Add to dismissed list
const newDismissed = new Set(dismissedWorkspaces);
newDismissed.add(unassociatedWorkspace.workspacePath);
setDismissedWorkspaces(newDismissed);
// Save to localStorage
localStorage.setItem('dismissedWorkspaces', JSON.stringify(Array.from(newDismissed)));
// Close dialog
setShowDialog(false);
setUnassociatedWorkspace(null);
toast.info('💡 We\'ll remind you next time you visit');
};
const handleLinkToProject = async (projectId: string) => {
if (!unassociatedWorkspace || !auth.currentUser) return;
setLoading(true);
try {
const response = await fetch('/api/sessions/associate-project', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
workspacePath: unassociatedWorkspace.workspacePath,
projectId,
userId: auth.currentUser.uid,
}),
});
if (response.ok) {
const data = await response.json();
toast.success(`✅ Linked ${data.sessionsUpdated} sessions to project!`);
setShowDialog(false);
setUnassociatedWorkspace(null);
} else {
toast.error('Failed to link sessions to project');
}
} catch (error) {
console.error('Error linking project:', error);
toast.error('An error occurred while linking');
} finally {
setLoading(false);
}
};
if (!unassociatedWorkspace) return null;
return (
<>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderOpen className="h-5 w-5" />
New Workspace Detected
</DialogTitle>
<DialogDescription>
We detected coding activity in a new workspace
</DialogDescription>
</DialogHeader>
{unassociatedWorkspace && (
<div className="my-4 p-4 bg-muted rounded-lg">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl">📂</span>
<div>
<p className="font-semibold">{unassociatedWorkspace?.workspaceName}</p>
<p className="text-xs text-muted-foreground font-mono truncate" title={unassociatedWorkspace?.workspacePath}>
{unassociatedWorkspace?.workspacePath}
</p>
</div>
</div>
<p className="text-sm text-muted-foreground mt-2">
{unassociatedWorkspace?.sessionCount} coding session{(unassociatedWorkspace?.sessionCount || 0) > 1 ? 's' : ''} tracked
</p>
</div>
)}
<div className="space-y-3">
<p className="text-sm font-medium">What would you like to do?</p>
<Button
className="w-full justify-start"
variant="outline"
onClick={handleCreateNewProject}
disabled={loading}
>
<Plus className="mr-2 h-4 w-4" />
Create New Project
</Button>
{projects.length > 0 && (
<>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or link to existing project
</span>
</div>
</div>
<div className="space-y-2 max-h-[200px] overflow-y-auto">
{projects.map((project) => (
<Button
key={project.id}
className="w-full justify-start"
variant="ghost"
onClick={() => handleLinkToProject(project.id)}
disabled={loading}
>
<LinkIcon className="mr-2 h-4 w-4" />
{project.productName}
</Button>
))}
</div>
</>
)}
</div>
<DialogFooter className="mt-4">
<Button variant="ghost" onClick={handleRemindLater} disabled={loading}>
Remind Me Later
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Project Creation Modal */}
<ProjectCreationModal
open={showCreationModal}
onOpenChange={(open) => {
setShowCreationModal(open);
if (!open) {
// Refresh to check for newly created project
setUnassociatedWorkspace(null);
}
}}
initialWorkspacePath={unassociatedWorkspace?.workspacePath}
workspace={workspace}
/>
</>
);
}