308 lines
9.9 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|