Spaces:
Build error
Build error
| 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> | |
| ); | |
| } | |