VIBN Frontend for Coolify deployment
This commit is contained in:
307
components/project-association-prompt.tsx
Normal file
307
components/project-association-prompt.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
'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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user