// Supplies the shared add/edit task form and submits API-ready values.
import { useEffect, useState } from 'react';
import { colors, inputStyle, primaryButtonStyle } from './theme';
const blankTask = { name: '', description: '', priority: 'Medium', detailedStatus: 'To Do', simpleStatus: 'Pending', dueDate: '', progress: 0 };
function TaskFormModal({ task, onClose, onSubmit }) {
const [form, setForm] = useState(blankTask);
const [saving, setSaving] = useState(false);
useEffect(() => { setForm(task ? { ...task, dueDate: task.dueDate.slice(0, 10) } : blankTask); }, [task]);
const change = ({ target }) => setForm((current) => ({ ...current, [target.name]: target.name === 'progress' ? Number(target.value) : target.value }));
const submit = async (event) => {
event.preventDefault(); setSaving(true);
try { await onSubmit(form); onClose(); } finally { setSaving(false); }
};
return (
<div className="modal d-block" tabIndex="-1" role="dialog" style={{ background: 'rgba(17,24,39,.45)' }}>
<div className="modal-dialog modal-dialog-centered">
<form className="modal-content border-0" style={{ borderRadius: 12 }} onSubmit={submit}>
<div className="modal-header" style={{ borderColor: colors.border }}><h2 className="modal-title" style={{ fontSize: 18, fontWeight: 600 }}>{task ? 'Edit Task' : 'Add New Task'}</h2><button type="button" className="btn-close" onClick={onClose} aria-label="Close" /></div>
<div className="modal-body p-4">
<label className="form-label small">Task name</label><input className="form-control mb-3" style={inputStyle} name="name" value={form.name} onChange={change} required />
<label className="form-label small">Description</label><input className="form-control mb-3" style={inputStyle} name="description" value={form.description} onChange={change} />
<div className="row g-3 mb-3"><div className="col-6"><label className="form-label small">Priority</label><select className="form-select" style={inputStyle} name="priority" value={form.priority} onChange={change}><option>High</option><option>Medium</option><option>Low</option></select></div><div className="col-6"><label className="form-label small">Status</label><select className="form-select" style={inputStyle} name="detailedStatus" value={form.detailedStatus} onChange={change}><option>To Do</option><option>In Progress</option><option>Review</option><option>Done</option></select></div></div>
<div className="row g-3"><div className="col-6"><label className="form-label small">Due date</label><input type="date" className="form-control" style={inputStyle} name="dueDate" value={form.dueDate} onChange={change} required /></div><div className="col-6"><label className="form-label small">Progress</label><input type="number" min="0" max="100" className="form-control" style={inputStyle} name="progress" value={form.progress} onChange={change} required /></div></div>
</div>
<div className="modal-footer" style={{ borderColor: colors.border }}><button type="button" className="btn btn-light" onClick={onClose}>Cancel</button><button type="submit" className="btn btn-primary" style={primaryButtonStyle} disabled={saving}>{saving ? 'Saving...' : 'Save Task'}</button></div>
</form>
</div>
</div>
);
}
export default TaskFormModal;
// Implements the searchable, filterable, paginated Tasks screen and CRUD actions.
import { useEffect, useMemo, useState } from 'react';
import { useOutletContext } from 'react-router-dom';
import TaskFilters from './TaskFilters';
import TaskCard from './TaskCard';
import TaskFormModal from './TaskFormModal';
import Pagination from './Pagination';
import { createTask, removeTask, replaceTask } from '../api/taskService';
import { colors, primaryButtonStyle } from './theme';
function Tasks() {
const { tasks = [], loading, error, refreshTasks } = useOutletContext() || {};
const [filters, setFilters] = useState({ search: '', priority: '', simpleStatus: '', tab: 'All' });
const [page, setPage] = useState(1);
const [modal, setModal] = useState(null);
const counts = useMemo(() => ({ All: tasks.length, 'To Do': tasks.filter((t) => t.detailedStatus === 'To Do').length, 'In Progress': tasks.filter((t) => t.detailedStatus === 'In Progress').length, Review: tasks.filter((t) => t.detailedStatus === 'Review').length, Done: tasks.filter((t) => t.detailedStatus === 'Done').length }), [tasks]);
const filtered = useMemo(() => tasks.filter((task) => {
const matchesSearch = task.name.toLowerCase().includes(filters.search.toLowerCase());
return matchesSearch && (!filters.priority || task.priority === filters.priority) && (!filters.simpleStatus || task.simpleStatus === filters.simpleStatus) && (filters.tab === 'All' || task.detailedStatus === filters.tab);
}), [tasks, filters]);
useEffect(() => { setPage(1); }, [filters]);
const visible = filtered.slice((page - 1) * 5, page * 5);
const save = async (form) => {
if (modal && modal !== 'new') await replaceTask(modal._id, form);
else await createTask(form);
await refreshTasks();
};
const destroy = async (task) => {
if (window.confirm(`Delete “${task.name}”?`)) { await removeTask(task._id); await refreshTasks(); }
};
return (
<><div className="d-flex justify-content-between align-items-start mb-4"><div><h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 5 }}>Tasks</h1><p style={{ color: colors.secondary, fontSize: 13, margin: 0 }}>Manage and organize all your tasks in one place.</p></div><button className="btn btn-primary" style={primaryButtonStyle} onClick={() => setModal('new')}>Add New Task</button></div>
{error && <div className="alert alert-danger">{error}</div>}<TaskFilters filters={filters} setFilters={setFilters} counts={counts} />
<div className="mt-3">{loading ? <div className="text-center py-5">Loading tasks...</div> : visible.map((task) => <TaskCard key={task._id} task={task} onEdit={setModal} onDelete={destroy} />)}</div>
<Pagination page={page} total={filtered.length} onChange={setPage} />
{modal && <TaskFormModal task={modal === 'new' ? null : modal} onClose={() => setModal(null)} onSubmit={save} />}
</>
);
}
export default Tasks;
// Centralizes the exact palette and reusable inline style values from the brief.
export const colors = {
sidebar: '#1E3A8A', active: '#2C4FC7', primary: '#2563EB', primaryHover: '#1D4ED8',
page: '#F9FAFB', surface: '#FFFFFF', border: '#E5E7EB', text: '#111827', secondary: '#6B7280',
};
export const surfaceStyle = { background: colors.surface, border: `1px solid ${colors.border}`, borderRadius: 12 };
export const inputStyle = { height: 40, borderRadius: 8, borderColor: '#D1D5DB', fontSize: 14 };
export const primaryButtonStyle = { background: colors.primary, borderColor: colors.primary, borderRadius: 8, padding: '10px 20px', fontSize: 14 };
// Displays the visual-only global search field and current user name.
import { colors } from './theme';
function Topbar() {
return (
<header style={{ height: 72, background: colors.surface, borderBottom: `1px solid ${colors.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 32px' }}>
<div className="d-flex align-items-center gap-4">
<i className="bi bi-list" style={{ fontSize: 22 }} aria-hidden="true" />
<div className="position-relative" style={{ width: 300 }}>
<i className="bi bi-search position-absolute" style={{ left: 13, top: 10, color: colors.secondary, fontSize: 13 }} aria-hidden="true" />
<input className="form-control border-0 bg-transparent" style={{ paddingLeft: 38, fontSize: 13 }} placeholder="Search tasks, projects..." aria-label="Global search" />
</div>
</div>
<div style={{ fontSize: 14, color: colors.text }}>John Doe <i className="bi bi-chevron-down ms-1" style={{ fontSize: 10 }} aria-hidden="true" /></div>
</header>
);
}
export default Topbar;