|
|
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(''); |
|
|
|
|
|
|
|
|
const [projectName, setProjectName] = useState(''); |
|
|
const [projectDescription, setProjectDescription] = useState(''); |
|
|
|
|
|
|
|
|
const [joinProjectId, setJoinProjectId] = useState(''); |
|
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false); |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const projects = await api.listProjects(user.id); |
|
|
const joinedProject = projects.projects?.find(p => p.id === result.project_id); |
|
|
|
|
|
if (joinedProject) { |
|
|
setCurrentProject(joinedProject); |
|
|
} else { |
|
|
|
|
|
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> |
|
|
); |
|
|
} |
|
|
|