« Back to History
app.js
|
20260722_120325.php
Initial Domain Snapshot
Copy Code
const { useEffect, useMemo, useState } = React; const EMPTY_FORM = { title: "", description: "", status: "todo", priority: "medium", day_start: "", day_end: "" }; function toEditableForm(task) { return { title: task.title || "", description: task.description || "", status: task.status || "todo", priority: task.priority || "medium", day_start: task.day_start ? String(task.day_start) : "", day_end: task.day_end ? String(task.day_end) : "" }; } function formatDateLabel(task) { // If both start and end are numbers, show as "4-8 each month" if (task.day_start) { if (task.day_end && task.day_end !== task.day_start) { return `${task.day_start}-${task.day_end} each month`; } return `${task.day_start} each month`; } // If schedule_start_date or due_date is present and valid, show with month const startDate = task.schedule_start_date || task.due_date; const endDate = task.schedule_end_date || startDate; if (!startDate) return "No schedule"; const start = new Date(`${startDate}T00:00:00`); const end = new Date(`${endDate}T00:00:00`); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) { return "No schedule"; } const monthLabel = start.toLocaleDateString(undefined, { month: "short" }); if (startDate === endDate) { return `${start.getDate()} ${monthLabel}`; } return `${start.getDate()}-${end.getDate()} ${monthLabel}`; } function bucketTasks(tasks) { const today = new Date(); today.setHours(0, 0, 0, 0); return tasks.reduce((groups, task) => { const startDateValue = task.schedule_start_date || task.due_date; const endDateValue = task.schedule_end_date || startDateValue; if (!startDateValue) { groups.future.push(task); return groups; } const startDate = new Date(`${startDateValue}T00:00:00`); const endDate = new Date(`${endDateValue}T00:00:00`); startDate.setHours(0, 0, 0, 0); endDate.setHours(0, 0, 0, 0); if (endDate.getTime() < today.getTime()) { groups.past.push(task); } else if (startDate.getTime() > today.getTime()) { groups.future.push(task); } else { groups.today.push(task); } return groups; }, { past: [], today: [], future: [] }); } function TaskCard({ task, onEdit, onDelete, deletingTaskId }) { return ( <article className={`task-card priority-${task.priority}`}> <div className="task-card-topline"> <span className="task-date">{formatDateLabel(task)}</span> <span className={`task-status status-${task.status}`}>{task.status.replace("_", " ")}</span> </div> <h3>{task.title}</h3> {task.description && <p>{task.description}</p>} <div className="task-card-footer"> <span className="task-priority">{task.priority}</span> <div className="task-card-actions"> <button type="button" className="card-button" onClick={() => onEdit(task)}> Edit </button> <button type="button" className="card-button card-button-danger" onClick={() => onDelete(task)} disabled={deletingTaskId === task.id}> {deletingTaskId === task.id ? "Deleting..." : "Delete"} </button> </div> </div> </article> ); } const { useRef } = React; function TaskLane({ label, hint, tasks, onEdit, onDelete, deletingTaskId }) { const railRef = useRef(null); // Robust scrollBy: fallback to scrollLeft if scrollBy not supported const scrollBy = (amount) => { const el = railRef.current; if (!el) return; if (typeof el.scrollBy === 'function') { el.scrollBy({ left: amount, behavior: "smooth" }); } else { el.scrollLeft += amount; } }; // Dull effect for past/future const dull = label === "Past" || label === "Future"; return ( <section className={`lane-section${dull ? " lane-dull" : ""}`} style={{position: 'relative'}}> <div className="lane-heading"> <div> <p className="lane-label">{label}</p> <h2>{hint}</h2> </div> <span className="lane-count">{tasks.length}</span> </div> <div className="task-rail-scroll-btns-fixed"> <button className="scroll-btn" type="button" onClick={() => scrollBy(-320)} title="Scroll left">←</button> <button className="scroll-btn" type="button" onClick={() => scrollBy(320)} title="Scroll right">→</button> </div> <div className="task-rail" ref={railRef}> {tasks.length ? tasks.map((task) => ( <TaskCard key={task.id} task={task} onEdit={onEdit} onDelete={onDelete} deletingTaskId={deletingTaskId} /> )) : <div className="empty-card">No tasks here yet.</div>} </div> </section> ); } function App() { const [tasks, setTasks] = useState([]); const [form, setForm] = useState(EMPTY_FORM); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [deletingTaskId, setDeletingTaskId] = useState(null); const [editingTaskId, setEditingTaskId] = useState(null); const [message, setMessage] = useState(""); async function fetchTasks() { setLoading(true); try { const response = await fetch("api/list_tasks.php"); const payload = await response.json(); if (!response.ok || !payload.success) { throw new Error(payload.message || "Unable to load tasks."); } setTasks(Array.isArray(payload.data?.tasks) ? payload.data.tasks : []); setMessage(""); } catch (error) { setMessage(error.message); } finally { setLoading(false); } } useEffect(() => { fetchTasks(); }, []); const groupedTasks = useMemo(() => bucketTasks(tasks), [tasks]); async function handleSubmit(event) { event.preventDefault(); setSaving(true); setMessage(""); try { const endpoint = editingTaskId ? "api/update_task.php" : "api/create_task.php"; const payloadBody = editingTaskId ? { ...form, id: editingTaskId } : form; const response = await fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payloadBody) }); const payload = await response.json(); if (!response.ok || !payload.success) { throw new Error(payload.message || "Unable to save task."); } setForm(EMPTY_FORM); setEditingTaskId(null); setMessage(payload.message || (editingTaskId ? "Task updated" : "Task created")); await fetchTasks(); } catch (error) { setMessage(error.message); } finally { setSaving(false); } } function startEdit(task) { setForm(toEditableForm(task)); setEditingTaskId(task.id); setMessage(`Editing ${task.title}`); } function cancelEdit() { setForm(EMPTY_FORM); setEditingTaskId(null); setMessage("Edit cancelled."); } async function handleDelete(task) { const shouldDelete = window.confirm(`Delete task: ${task.title}?`); if (!shouldDelete) { return; } setDeletingTaskId(task.id); setMessage(""); try { const response = await fetch("api/delete_task.php", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: task.id }) }); const payload = await response.json(); if (!response.ok || !payload.success) { throw new Error(payload.message || "Unable to delete task."); } if (editingTaskId === task.id) { setForm(EMPTY_FORM); setEditingTaskId(null); } setMessage(payload.message || "Task deleted"); await fetchTasks(); } catch (error) { setMessage(error.message); } finally { setDeletingTaskId(null); } } function updateForm(event) { const { name, value } = event.target; setForm((current) => ({ ...current, [name]: value })); } return ( <div className="pul-app"> <header className="masthead"> <div> <p className="eyebrow">Pul</p> <h1>River timeline for active work.</h1> <p className="masthead-copy">Stand on today, see what passed behind, and what is drifting toward you next.</p> </div> <button className="ghost-button" type="button" onClick={fetchTasks} disabled={loading}> {loading ? "Syncing..." : "Refresh"} </button> </header> <section className="river-board"> <div className="river-layer river-layer-top"></div> <div className="river-layer river-layer-bottom"></div> <LaneBadge align="left" title="Past" text="Completed time behind you" /> <LaneBadge align="right" title="Future" text="Incoming work ahead" /> <div className="bridge-marker"> <span>Today</span> <strong>{new Date().toLocaleDateString(undefined, { day: "numeric", month: "short" })}</strong> </div> <div className="bridge-column"> <TaskLane label="Past" hint="Behind the bridge" tasks={groupedTasks.past} onEdit={startEdit} onDelete={handleDelete} deletingTaskId={deletingTaskId} /> <TaskLane label="Today" hint="On the bridge" tasks={groupedTasks.today} onEdit={startEdit} onDelete={handleDelete} deletingTaskId={deletingTaskId} /> <TaskLane label="Future" hint="Ahead of the bridge" tasks={groupedTasks.future} onEdit={startEdit} onDelete={handleDelete} deletingTaskId={deletingTaskId} /> </div> </section> <section className="composer-shell"> <div className="composer-copy"> <p className="eyebrow">{editingTaskId ? "Edit task" : "Create task"}</p> <h2>{editingTaskId ? "Update your monthly flow." : "Define your fixed monthly flow."}</h2> <p>{message || "Use one day for a single-date task, or add an end day for a date range like 4-8 every month."}</p> </div> <form className="task-form" onSubmit={handleSubmit}> <label> <span>Title</span> <input name="title" value={form.title} onChange={updateForm} maxLength="150" required /> </label> <label className="field-wide"> <span>Description</span> <textarea name="description" value={form.description} onChange={updateForm} rows="4" maxLength="1000" /> </label> <label> <span>Status</span> <select name="status" value={form.status} onChange={updateForm}> <option value="todo">Todo</option> <option value="in_progress">In Progress</option> <option value="done">Done</option> </select> </label> <label> <span>Priority</span> <select name="priority" value={form.priority} onChange={updateForm}> <option value="low">Low</option> <option value="medium">Medium</option> <option value="high">High</option> </select> </label> <label> <span>Start day</span> <input type="number" min="1" max="31" name="day_start" value={form.day_start} onChange={updateForm} required /> </label> <label> <span>End day</span> <input type="number" min="1" max="31" name="day_end" value={form.day_end} onChange={updateForm} placeholder="Optional" /> </label> <div className="form-actions field-wide"> <button className="primary-button" type="submit" disabled={saving}> {saving ? "Saving..." : editingTaskId ? "Update task" : "Save task"} </button> {editingTaskId ? ( <button className="ghost-button" type="button" onClick={cancelEdit}> Cancel edit </button> ) : null} </div> </form> </section> </div> ); } function LaneBadge({ align, title, text }) { return ( <div className={`lane-badge lane-badge-${align}`}> <span>{title}</span> <p>{text}</p> </div> ); } ReactDOM.createRoot(document.getElementById("root")).render(<App />);