feat: enhanced create modal, project/assignee badges, status filter, keyboard shortcuts

- CreateTaskModal: project selector, due date picker, source in 'more options'
- TaskCard: project name badge (📁) and assignee badge (👤)
- QueuePage: status filter dropdown, clear filters button, filter count indicator
- QueuePage: Ctrl+N keyboard shortcut to create task
- DashboardPage: project/assignee badges on active task cards
- Search now also matches assignee name
This commit is contained in:
2026-01-29 08:03:47 +00:00
parent 578b092a78
commit f00e0720e1
5 changed files with 368 additions and 125 deletions

View File

@@ -1,4 +1,6 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { fetchProjects } from "../lib/api";
import type { Project } from "../lib/types";
interface CreateTaskModalProps {
open: boolean;
@@ -8,6 +10,8 @@ interface CreateTaskModalProps {
description?: string;
source?: string;
priority?: string;
projectId?: string;
dueDate?: string;
}) => void;
}
@@ -16,6 +20,26 @@ export function CreateTaskModal({ open, onClose, onCreate }: CreateTaskModalProp
const [description, setDescription] = useState("");
const [source, setSource] = useState("donovan");
const [priority, setPriority] = useState("medium");
const [projectId, setProjectId] = useState("");
const [dueDate, setDueDate] = useState("");
const [projects, setProjects] = useState<Project[]>([]);
const [showAdvanced, setShowAdvanced] = useState(false);
useEffect(() => {
if (open) {
fetchProjects().then(setProjects).catch(() => {});
}
}, [open]);
// Keyboard shortcut: Escape to close
useEffect(() => {
if (!open) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [open, onClose]);
if (!open) return null;
@@ -27,75 +51,166 @@ export function CreateTaskModal({ open, onClose, onCreate }: CreateTaskModalProp
description: description.trim() || undefined,
source,
priority,
projectId: projectId || undefined,
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
});
// Reset form
setTitle("");
setDescription("");
setSource("donovan");
setPriority("medium");
setProjectId("");
setDueDate("");
setShowAdvanced(false);
onClose();
};
return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-md mx-4">
<h2 className="text-lg font-bold mb-4">New Task</h2>
<form onSubmit={handleSubmit} className="space-y-3">
<input
type="text"
placeholder="Task title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400"
autoFocus
/>
<textarea
placeholder="Description / context (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400"
/>
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold">New Task</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 p-1 rounded-lg hover:bg-gray-100 transition"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Title */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Title</label>
<input
type="text"
placeholder="What needs to be done?"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-amber-400"
autoFocus
/>
</div>
{/* Description */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Description</label>
<textarea
placeholder="Context, requirements, links... (optional)"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 focus:border-amber-400 resize-y"
/>
</div>
{/* Priority & Source row */}
<div className="flex gap-3">
<div className="flex-1">
<label className="text-xs text-gray-500 block mb-1">Source</label>
<select
value={source}
onChange={(e) => setSource(e.target.value)}
className="w-full border rounded-lg px-3 py-2 text-sm"
>
<option value="donovan">Donovan</option>
<option value="david">David</option>
<option value="hammer">Hammer</option>
<option value="heartbeat">Heartbeat</option>
<option value="cron">Cron</option>
<option value="other">Other</option>
</select>
</div>
<div className="flex-1">
<label className="text-xs text-gray-500 block mb-1">Priority</label>
<select
value={priority}
onChange={(e) => setPriority(e.target.value)}
className="w-full border rounded-lg px-3 py-2 text-sm"
>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<label className="text-xs font-medium text-gray-500 block mb-1">Priority</label>
<div className="flex gap-1">
{(["critical", "high", "medium", "low"] as const).map((p) => (
<button
key={p}
type="button"
onClick={() => setPriority(p)}
className={`flex-1 text-xs py-2 rounded-lg font-medium transition border ${
priority === p
? p === "critical" ? "bg-red-500 text-white border-red-500"
: p === "high" ? "bg-orange-500 text-white border-orange-500"
: p === "medium" ? "bg-blue-500 text-white border-blue-500"
: "bg-gray-500 text-white border-gray-500"
: "bg-white text-gray-500 border-gray-200 hover:border-gray-300 hover:bg-gray-50"
}`}
>
{p === "critical" ? "🔴" : p === "high" ? "🟠" : p === "medium" ? "🔵" : "⚪"} {p[0].toUpperCase() + p.slice(1)}
</button>
))}
</div>
</div>
</div>
{/* Project selector */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Project</label>
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="">No project</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
{/* Advanced toggle */}
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-xs text-gray-400 hover:text-gray-600 flex items-center gap-1 transition"
>
{showAdvanced ? "▾" : "▸"} More options
</button>
{showAdvanced && (
<div className="space-y-3 pl-2 border-l-2 border-gray-100">
{/* Due Date */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Due Date</label>
<div className="flex items-center gap-2">
<input
type="datetime-local"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
/>
{dueDate && (
<button
type="button"
onClick={() => setDueDate("")}
className="text-xs text-gray-400 hover:text-red-500 transition"
>
</button>
)}
</div>
</div>
{/* Source */}
<div>
<label className="text-xs font-medium text-gray-500 block mb-1">Source</label>
<select
value={source}
onChange={(e) => setSource(e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white"
>
<option value="donovan">Donovan</option>
<option value="david">David</option>
<option value="hammer">Hammer</option>
<option value="heartbeat">Heartbeat</option>
<option value="cron">Cron</option>
<option value="other">Other</option>
</select>
</div>
</div>
)}
{/* Submit buttons */}
<div className="flex gap-2 pt-2">
<button
type="submit"
className="flex-1 bg-amber-500 text-white rounded-lg py-2 text-sm font-medium hover:bg-amber-600 transition"
disabled={!title.trim()}
className="flex-1 bg-amber-500 text-white rounded-lg py-2.5 text-sm font-semibold hover:bg-amber-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Create Task
</button>
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900"
className="px-4 py-2.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition"
>
Cancel
</button>