|
|
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[]>([]); |
|
|
|
|
|
|
|
|
const [manualTitle, setManualTitle] = useState(''); |
|
|
const [manualDescription, setManualDescription] = useState(''); |
|
|
|
|
|
|
|
|
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> |
|
|
|
|
|
{} |
|
|
<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> |
|
|
); |
|
|
} |
|
|
|