Initial scaffold: Hammer Queue task dashboard

- Backend: Elysia + Bun + Drizzle ORM + PostgreSQL
- Frontend: React + Vite + TypeScript + Tailwind CSS
- Task CRUD API with bearer token auth for writes
- Public read-only dashboard with auto-refresh
- Task states: active, queued, blocked, completed, cancelled
- Reorder support for queue management
- Progress notes per task
- Docker Compose for local dev and Dokploy deployment
This commit is contained in:
2026-01-28 22:55:16 +00:00
commit 0a8d5486bb
36 changed files with 2210 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
import { useState } from "react";
interface CreateTaskModalProps {
open: boolean;
onClose: () => void;
onCreate: (task: {
title: string;
description?: string;
source?: string;
priority?: string;
}) => void;
}
export function CreateTaskModal({ open, onClose, onCreate }: CreateTaskModalProps) {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [source, setSource] = useState("donovan");
const [priority, setPriority] = useState("medium");
if (!open) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
onCreate({
title: title.trim(),
description: description.trim() || undefined,
source,
priority,
});
setTitle("");
setDescription("");
setSource("donovan");
setPriority("medium");
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="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>
</div>
</div>
<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"
>
Create Task
</button>
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-900"
>
Cancel
</button>
</div>
</form>
</div>
</div>
);
}