ProjectMemory / frontend /src /pages /ProjectSelectionPage.tsx
Amal Nimmy Lal
feat : Project Memory
35765b5
import { useState, useEffect } from 'react';
import { useUser } from '../context/UserContext';
import { useProject } from '../context/ProjectContext';
import { api } from '../api/client';
import type { Project } from '../types';
type Mode = 'select' | 'create' | 'join';
export function ProjectSelectionPage() {
const { user, logout } = useUser();
const { setCurrentProject } = useProject();
const [mode, setMode] = useState<Mode>('select');
const [existingProjects, setExistingProjects] = useState<Project[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
// Create form state
const [projectName, setProjectName] = useState('');
const [projectDescription, setProjectDescription] = useState('');
// Join form state
const [joinProjectId, setJoinProjectId] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Fetch existing projects on mount
useEffect(() => {
if (!user) return;
const fetchProjects = async () => {
try {
const response = await api.listProjects(user.id);
setExistingProjects(response.projects || []);
} catch (err) {
console.error('Failed to fetch projects:', err);
} finally {
setIsLoading(false);
}
};
fetchProjects();
}, [user]);
const handleSelectProject = (project: Project) => {
setCurrentProject(project);
};
const handleCreateProject = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!projectName.trim()) {
setError('Project name is required');
return;
}
if (!user) return;
setIsSubmitting(true);
try {
// Check if project name is available (since name = id)
const availability = await api.checkProjectAvailability(projectName.trim());
if (!availability.available) {
setError('Project name already taken. Try a different name.');
setIsSubmitting(false);
return;
}
const project = await api.createProject({
name: projectName.trim(),
description: projectDescription.trim(),
userId: user.id,
});
setCurrentProject({ ...project, role: 'owner' });
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create project');
} finally {
setIsSubmitting(false);
}
};
const handleJoinProject = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (!joinProjectId.trim()) {
setError('Project ID is required');
return;
}
if (!user) return;
setIsSubmitting(true);
try {
const result = await api.joinProject(joinProjectId.trim(), user.id);
// Fetch the full project details
const projects = await api.listProjects(user.id);
const joinedProject = projects.projects?.find(p => p.id === result.project_id);
if (joinedProject) {
setCurrentProject(joinedProject);
} else {
// Fallback if project not found in list
setCurrentProject({
id: result.project_id,
name: result.project_id,
description: '',
created_at: new Date().toISOString(),
role: result.role,
});
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join project');
} finally {
setIsSubmitting(false);
}
};
if (!user) return null;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
{/* Header */}
<header className="bg-white/5 backdrop-blur-lg border-b border-white/10">
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 className="text-xl font-bold text-white">Project Memory</h1>
<div className="flex items-center gap-4">
<span className="text-purple-300 text-sm">{user.firstName} ({user.id})</span>
<button
onClick={logout}
className="px-3 py-1.5 bg-white/10 hover:bg-white/20 text-white rounded-lg transition-all text-sm"
>
Logout
</button>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-4xl mx-auto px-4 py-8">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-white mb-2">Select a Project</h2>
<p className="text-purple-300">Choose an existing project or start fresh</p>
</div>
{/* Error Display */}
{error && (
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-200 text-center">
{error}
</div>
)}
{/* Loading State */}
{isLoading ? (
<div className="text-center text-purple-300 py-12">Loading projects...</div>
) : (
<>
{/* Existing Projects */}
{existingProjects.length > 0 && mode === 'select' && (
<div className="mb-8">
<h3 className="text-lg font-semibold text-white mb-4">Your Projects</h3>
<div className="grid gap-4 md:grid-cols-2">
{existingProjects.map((project) => (
<button
key={project.id}
onClick={() => handleSelectProject(project)}
className="text-left p-4 bg-white/10 hover:bg-white/20 border border-white/20 hover:border-purple-500/50 rounded-xl transition-all group"
>
<div className="flex items-start justify-between">
<div>
<h4 className="text-white font-medium group-hover:text-purple-300 transition-colors">
{project.name}
</h4>
<p className="text-purple-300/70 text-sm mt-1 line-clamp-2">
{project.description || 'No description'}
</p>
</div>
<span className="text-xs px-2 py-1 bg-purple-500/20 text-purple-300 rounded">
{project.role}
</span>
</div>
</button>
))}
</div>
</div>
)}
{/* Action Buttons (when in select mode) */}
{mode === 'select' && (
<div className="grid gap-4 md:grid-cols-2">
<button
onClick={() => setMode('create')}
className="p-6 bg-purple-600/20 hover:bg-purple-600/30 border-2 border-dashed border-purple-500/50 hover:border-purple-500 rounded-xl transition-all text-center"
>
<div className="text-4xl mb-2">+</div>
<h4 className="text-white font-medium">Create New Project</h4>
<p className="text-purple-300/70 text-sm mt-1">Start a new team project</p>
</button>
<button
onClick={() => setMode('join')}
className="p-6 bg-white/5 hover:bg-white/10 border-2 border-dashed border-white/20 hover:border-white/40 rounded-xl transition-all text-center"
>
<div className="text-4xl mb-2"></div>
<h4 className="text-white font-medium">Join Existing Project</h4>
<p className="text-purple-300/70 text-sm mt-1">Enter a project ID to join</p>
</button>
</div>
)}
{/* Create Project Form */}
{mode === 'create' && (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">Create New Project</h3>
<button
onClick={() => { setMode('select'); setError(''); }}
className="text-purple-300 hover:text-white text-sm"
>
← Back
</button>
</div>
<form onSubmit={handleCreateProject} className="space-y-4">
<div>
<label htmlFor="projectName" className="block text-sm font-medium text-purple-200 mb-1">
Project Name (also used as ID)
</label>
<input
id="projectName"
type="text"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
placeholder="e.g., fastgate"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isSubmitting}
/>
<p className="text-purple-300/50 text-xs mt-1">Must be unique. This will be the project ID.</p>
</div>
<div>
<label htmlFor="projectDescription" className="block text-sm font-medium text-purple-200 mb-1">
Description
</label>
<textarea
id="projectDescription"
value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)}
placeholder="What is this project about?"
rows={3}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none"
disabled={isSubmitting}
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3 px-4 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-all"
>
{isSubmitting ? 'Creating...' : 'Create Project'}
</button>
</form>
</div>
)}
{/* Join Project Form */}
{mode === 'join' && (
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border border-white/20">
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">Join Existing Project</h3>
<button
onClick={() => { setMode('select'); setError(''); }}
className="text-purple-300 hover:text-white text-sm"
>
← Back
</button>
</div>
<form onSubmit={handleJoinProject} className="space-y-4">
<div>
<label htmlFor="joinProjectId" className="block text-sm font-medium text-purple-200 mb-1">
Project ID
</label>
<input
id="joinProjectId"
type="text"
value={joinProjectId}
onChange={(e) => setJoinProjectId(e.target.value)}
placeholder="Enter the project ID"
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isSubmitting}
/>
<p className="text-purple-300/50 text-xs mt-1">Ask your team lead for the project ID</p>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3 px-4 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-all"
>
{isSubmitting ? 'Joining...' : 'Join Project'}
</button>
</form>
</div>
)}
</>
)}
</main>
</div>
);
}