ProjectMemory / frontend /src /pages /TaskSetupPage.tsx
Amal Nimmy Lal
feat : Project Memory
35765b5
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>
);
}