Spaces:
Build error
Build error
| import { useState } from 'react'; | |
| import { useUser } from '../context/UserContext'; | |
| import { useProject } from '../context/ProjectContext'; | |
| import { api } from '../api/client'; | |
| interface LocalTask { | |
| id: string; | |
| title: string; | |
| description: string; | |
| } | |
| export function TaskSetupPage({ onComplete }: { onComplete: () => void }) { | |
| const { user, logout } = useUser(); | |
| const { currentProject, clearProject } = useProject(); | |
| const [tasks, setTasks] = useState<LocalTask[]>([]); | |
| // Manual form state | |
| const [manualTitle, setManualTitle] = useState(''); | |
| const [manualDescription, setManualDescription] = useState(''); | |
| // Edit state | |
| const [editingId, setEditingId] = useState<string | null>(null); | |
| const [editTitle, setEditTitle] = useState(''); | |
| const [editDescription, setEditDescription] = useState(''); | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [isSaving, setIsSaving] = useState(false); | |
| const [error, setError] = useState(''); | |
| const [saveProgress, setSaveProgress] = useState({ current: 0, total: 0 }); | |
| const handleLogout = () => { | |
| clearProject(); | |
| logout(); | |
| }; | |
| const generateId = () => Math.random().toString(36).substring(2, 9); | |
| const handleAddManualTask = () => { | |
| if (!manualTitle.trim()) return; | |
| setTasks([ | |
| ...tasks, | |
| { | |
| id: generateId(), | |
| title: manualTitle.trim(), | |
| description: manualDescription.trim(), | |
| }, | |
| ]); | |
| setManualTitle(''); | |
| setManualDescription(''); | |
| }; | |
| const handleGenerateTasks = async () => { | |
| if (!currentProject) return; | |
| setIsGenerating(true); | |
| setError(''); | |
| try { | |
| const result = await api.generateTasks(currentProject.id, 50); | |
| const newTasks = result.tasks.map((t) => ({ | |
| id: generateId(), | |
| title: t.title, | |
| description: t.description, | |
| })); | |
| setTasks([...tasks, ...newTasks]); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to generate tasks'); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; | |
| const handleDeleteTask = (id: string) => { | |
| setTasks(tasks.filter((t) => t.id !== id)); | |
| }; | |
| const handleStartEdit = (task: LocalTask) => { | |
| setEditingId(task.id); | |
| setEditTitle(task.title); | |
| setEditDescription(task.description); | |
| }; | |
| const handleSaveEdit = () => { | |
| if (!editingId) return; | |
| setTasks( | |
| tasks.map((t) => | |
| t.id === editingId | |
| ? { ...t, title: editTitle.trim(), description: editDescription.trim() } | |
| : t | |
| ) | |
| ); | |
| setEditingId(null); | |
| setEditTitle(''); | |
| setEditDescription(''); | |
| }; | |
| const handleCancelEdit = () => { | |
| setEditingId(null); | |
| setEditTitle(''); | |
| setEditDescription(''); | |
| }; | |
| const handleSaveAndContinue = async () => { | |
| if (tasks.length === 0 || !currentProject) return; | |
| setIsSaving(true); | |
| setError(''); | |
| setSaveProgress({ current: 0, total: tasks.length }); | |
| try { | |
| for (let i = 0; i < tasks.length; i++) { | |
| const task = tasks[i]; | |
| await api.createTask(currentProject.id, { | |
| title: task.title, | |
| description: task.description, | |
| }); | |
| setSaveProgress({ current: i + 1, total: tasks.length }); | |
| } | |
| onComplete(); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Failed to save tasks'); | |
| } finally { | |
| setIsSaving(false); | |
| } | |
| }; | |
| const handleClearAll = () => { | |
| setTasks([]); | |
| }; | |
| if (!user || !currentProject) 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-6xl mx-auto px-4 py-4 flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <h1 className="text-xl font-bold text-white">Project Memory</h1> | |
| <span className="text-purple-300/50">|</span> | |
| <span className="text-purple-300">{currentProject.name}</span> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <span className="text-purple-300 text-sm"> | |
| {user.firstName} ({user.id}) | |
| </span> | |
| <button | |
| onClick={handleLogout} | |
| 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-6xl mx-auto px-4 py-6"> | |
| {/* Page Title */} | |
| <div className="mb-6"> | |
| <h2 className="text-2xl font-bold text-white mb-1">Set Up Tasks</h2> | |
| <p className="text-purple-300 text-sm"> | |
| Add tasks manually or generate demo tasks with AI (max 50) | |
| </p> | |
| </div> | |
| {/* Error Display */} | |
| {error && ( | |
| <div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-200 text-sm"> | |
| {error} | |
| </div> | |
| )} | |
| {/* Two Column Layout */} | |
| <div className="grid lg:grid-cols-3 gap-6"> | |
| {/* Left Column - Add Tasks */} | |
| <div className="lg:col-span-1 space-y-4"> | |
| {/* Manual Add */} | |
| <div className="bg-white/10 backdrop-blur-lg rounded-xl p-4 border border-white/20"> | |
| <h3 className="text-white font-medium mb-3">Add Task</h3> | |
| <div className="space-y-3"> | |
| <input | |
| type="text" | |
| value={manualTitle} | |
| onChange={(e) => setManualTitle(e.target.value)} | |
| placeholder="Task title" | |
| className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm" | |
| onKeyDown={(e) => e.key === 'Enter' && handleAddManualTask()} | |
| /> | |
| <textarea | |
| value={manualDescription} | |
| onChange={(e) => setManualDescription(e.target.value)} | |
| placeholder="Description (optional)" | |
| rows={2} | |
| className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none text-sm" | |
| /> | |
| <button | |
| onClick={handleAddManualTask} | |
| disabled={!manualTitle.trim()} | |
| className="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 disabled:cursor-not-allowed text-white rounded-lg transition-all text-sm" | |
| > | |
| + Add Task | |
| </button> | |
| </div> | |
| </div> | |
| {/* AI Generate */} | |
| <div className="bg-purple-600/20 backdrop-blur-lg rounded-xl p-4 border border-purple-500/30"> | |
| <h3 className="text-white font-medium mb-2">AI Generate</h3> | |
| <p className="text-purple-300/70 text-xs mb-3"> | |
| Generate 50 demo tasks based on your project | |
| </p> | |
| <button | |
| onClick={handleGenerateTasks} | |
| disabled={isGenerating} | |
| className="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 text-white rounded-lg transition-all text-sm flex items-center justify-center gap-2" | |
| > | |
| {isGenerating ? ( | |
| <> | |
| <span className="animate-spin">⏳</span> Generating... | |
| </> | |
| ) : ( | |
| <>✨ Generate Demo Tasks</> | |
| )} | |
| </button> | |
| </div> | |
| {/* Actions */} | |
| {tasks.length > 0 && ( | |
| <div className="space-y-2"> | |
| <button | |
| onClick={handleSaveAndContinue} | |
| disabled={isSaving} | |
| className="w-full px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-800 text-white font-medium rounded-lg transition-all flex items-center justify-center gap-2" | |
| > | |
| {isSaving ? ( | |
| <>Saving... ({saveProgress.current}/{saveProgress.total})</> | |
| ) : ( | |
| <>Save & Continue →</> | |
| )} | |
| </button> | |
| <button | |
| onClick={handleClearAll} | |
| disabled={isSaving} | |
| className="w-full px-3 py-2 bg-white/5 hover:bg-white/10 text-red-400 hover:text-red-300 rounded-lg transition-all text-sm" | |
| > | |
| Clear All Tasks | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Right Column - Task List */} | |
| <div className="lg:col-span-2"> | |
| <div className="bg-white/5 backdrop-blur-lg rounded-xl border border-white/10 overflow-hidden"> | |
| <div className="px-4 py-3 border-b border-white/10 flex items-center justify-between"> | |
| <h3 className="text-white font-medium"> | |
| Tasks ({tasks.length}) | |
| </h3> | |
| {tasks.length > 0 && ( | |
| <span className="text-purple-300/50 text-xs"> | |
| Click ✏️ to edit, 🗑️ to delete | |
| </span> | |
| )} | |
| </div> | |
| {tasks.length === 0 ? ( | |
| <div className="p-8 text-center text-purple-300/50"> | |
| <div className="text-4xl mb-2">📋</div> | |
| <p>No tasks yet</p> | |
| <p className="text-xs mt-1">Add manually or generate with AI</p> | |
| </div> | |
| ) : ( | |
| <div className="max-h-[60vh] overflow-y-auto"> | |
| {tasks.map((task, index) => ( | |
| <div | |
| key={task.id} | |
| className="px-4 py-3 border-b border-white/5 hover:bg-white/5 transition-colors" | |
| > | |
| {editingId === task.id ? ( | |
| <div className="space-y-2"> | |
| <input | |
| type="text" | |
| value={editTitle} | |
| onChange={(e) => setEditTitle(e.target.value)} | |
| className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:ring-2 focus:ring-purple-500 text-sm" | |
| autoFocus | |
| /> | |
| <textarea | |
| value={editDescription} | |
| onChange={(e) => setEditDescription(e.target.value)} | |
| rows={2} | |
| className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:ring-2 focus:ring-purple-500 resize-none text-sm" | |
| /> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={handleSaveEdit} | |
| className="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-xs" | |
| > | |
| Save | |
| </button> | |
| <button | |
| onClick={handleCancelEdit} | |
| className="px-3 py-1.5 bg-white/10 hover:bg-white/20 text-white rounded text-xs" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className="flex items-start justify-between gap-3"> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-purple-400 text-xs font-mono"> | |
| #{index + 1} | |
| </span> | |
| <h4 className="text-white text-sm font-medium truncate"> | |
| {task.title} | |
| </h4> | |
| </div> | |
| {task.description && ( | |
| <p className="text-purple-300/60 text-xs mt-0.5 line-clamp-1"> | |
| {task.description} | |
| </p> | |
| )} | |
| </div> | |
| <div className="flex gap-1 flex-shrink-0"> | |
| <button | |
| onClick={() => handleStartEdit(task)} | |
| className="p-1 text-purple-300 hover:text-white hover:bg-white/10 rounded transition-all" | |
| title="Edit" | |
| > | |
| ✏️ | |
| </button> | |
| <button | |
| onClick={() => handleDeleteTask(task.id)} | |
| className="p-1 text-red-400 hover:text-red-300 hover:bg-white/10 rounded transition-all" | |
| title="Delete" | |
| > | |
| 🗑️ | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| } | |