feat: toast notifications, delete task, due date badges on cards, keyboard shortcuts
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
import type { Task, TaskStatus, Project } from "../lib/types";
|
||||
import { updateTask, fetchProjects, addProgressNote, addSubtask, toggleSubtask, deleteSubtask } from "../lib/api";
|
||||
import { updateTask, fetchProjects, addProgressNote, addSubtask, toggleSubtask, deleteSubtask, deleteTask } from "../lib/api";
|
||||
import { useToast } from "../components/Toast";
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
critical: "bg-red-500 text-white",
|
||||
@@ -73,6 +74,10 @@ export function TaskPage() {
|
||||
const [newSubtaskTitle, setNewSubtaskTitle] = useState("");
|
||||
const [addingSubtask, setAddingSubtask] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const fetchTask = useCallback(async () => {
|
||||
if (!taskRef) return;
|
||||
@@ -106,13 +111,31 @@ export function TaskPage() {
|
||||
try {
|
||||
await updateTask(task.id, { status });
|
||||
fetchTask();
|
||||
toast(`Task moved to ${status}`, "success");
|
||||
} catch (e) {
|
||||
console.error("Failed to update status:", e);
|
||||
toast("Failed to update status", "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!task) return;
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteTask(task.id);
|
||||
toast("Task deleted", "success");
|
||||
navigate("/queue");
|
||||
} catch (e) {
|
||||
console.error("Failed to delete:", e);
|
||||
toast("Failed to delete task", "error");
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
setShowDeleteConfirm(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center text-gray-400">
|
||||
@@ -478,6 +501,7 @@ export function TaskPage() {
|
||||
onClick={() => {
|
||||
const url = `${window.location.origin}/task/HQ-${task.taskNumber}`;
|
||||
navigator.clipboard.writeText(url);
|
||||
toast("Link copied", "info");
|
||||
}}
|
||||
className="text-xs px-2 py-1 bg-gray-100 rounded hover:bg-gray-200 transition"
|
||||
>
|
||||
@@ -485,6 +509,38 @@ export function TaskPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm p-4">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Danger Zone</h3>
|
||||
{!showDeleteConfirm ? (
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="w-full text-sm text-red-600 hover:text-red-700 border border-red-200 rounded-lg px-3 py-2 hover:bg-red-50 transition font-medium"
|
||||
>
|
||||
🗑 Delete Task
|
||||
</button>
|
||||
) : (
|
||||
<div className="space-y-2 animate-slide-up">
|
||||
<p className="text-xs text-red-700">This cannot be undone. Are you sure?</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="flex-1 text-sm px-3 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 transition disabled:opacity-50"
|
||||
>
|
||||
{deleting ? "Deleting..." : "Yes, delete"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
className="flex-1 text-sm px-3 py-2 border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50 transition"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user