feat: Hammer Dashboard with sidebar navigation (HQ-21)

- Add React Router with sidebar layout (DashboardLayout)
- Queue is now a routed page at /queue
- Chat placeholder page at /chat
- Admin page accessible from sidebar
- Dark sidebar with amber accent for active nav
- Updated CORS and auth to support dash.donovankelly.xyz
- Renamed to Hammer Dashboard
This commit is contained in:
2026-01-29 02:14:25 +00:00
parent 5c2e372ed2
commit 91bc69e178
11 changed files with 362 additions and 253 deletions

View File

@@ -0,0 +1,208 @@
import { useState, useMemo } from "react";
import { useTasks } from "../hooks/useTasks";
import { useCurrentUser } from "../hooks/useCurrentUser";
import { TaskCard } from "../components/TaskCard";
import { TaskDetailPanel } from "../components/TaskDetailPanel";
import { CreateTaskModal } from "../components/CreateTaskModal";
import { updateTask, reorderTasks, createTask } from "../lib/api";
import type { TaskStatus } from "../lib/types";
export function QueuePage() {
const { tasks, loading, error, refresh } = useTasks(5000);
const { isAuthenticated } = useCurrentUser();
const [showCreate, setShowCreate] = useState(false);
const [showCompleted, setShowCompleted] = useState(false);
const [selectedTask, setSelectedTask] = useState<string | null>(null);
const selectedTaskData = useMemo(() => {
if (!selectedTask) return null;
return tasks.find((t) => t.id === selectedTask) || null;
}, [tasks, selectedTask]);
const activeTasks = useMemo(() => tasks.filter((t) => t.status === "active"), [tasks]);
const queuedTasks = useMemo(() => tasks.filter((t) => t.status === "queued"), [tasks]);
const blockedTasks = useMemo(() => tasks.filter((t) => t.status === "blocked"), [tasks]);
const completedTasks = useMemo(
() => tasks.filter((t) => t.status === "completed" || t.status === "cancelled"),
[tasks]
);
const handleStatusChange = async (id: string, status: TaskStatus) => {
try {
await updateTask(id, { status });
refresh();
} catch (e) {
alert("Failed to update task.");
}
};
const handleMoveUp = async (index: number) => {
if (index === 0) return;
const ids = queuedTasks.map((t) => t.id);
[ids[index - 1], ids[index]] = [ids[index], ids[index - 1]];
await reorderTasks(ids);
refresh();
};
const handleMoveDown = async (index: number) => {
if (index >= queuedTasks.length - 1) return;
const ids = queuedTasks.map((t) => t.id);
[ids[index], ids[index + 1]] = [ids[index + 1], ids[index]];
await reorderTasks(ids);
refresh();
};
const handleCreate = async (task: {
title: string;
description?: string;
source?: string;
priority?: string;
}) => {
await createTask(task);
refresh();
};
return (
<div className="min-h-screen">
{/* Page Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-30">
<div className="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-gray-900">Task Queue</h1>
<p className="text-sm text-gray-400">Manage what Hammer is working on</p>
</div>
<button
onClick={() => setShowCreate(true)}
className="text-sm bg-amber-500 text-white px-4 py-2 rounded-lg hover:bg-amber-600 transition font-medium"
>
+ New Task
</button>
</div>
</header>
<CreateTaskModal
open={showCreate}
onClose={() => setShowCreate(false)}
onCreate={handleCreate}
/>
<div className="max-w-4xl mx-auto px-6 py-6 space-y-6">
{loading && (
<div className="text-center text-gray-400 py-12">Loading tasks...</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-3 text-sm">
{error}
</div>
)}
{/* Active Task */}
<section>
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
Currently Working On
</h2>
{activeTasks.length === 0 ? (
<div className="border-2 border-dashed border-gray-200 rounded-lg p-8 text-center text-gray-400">
No active task Hammer is idle
</div>
) : (
<div className="space-y-3">
{activeTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
isActive
onClick={() => setSelectedTask(task.id)}
/>
))}
</div>
)}
</section>
{/* Blocked */}
{blockedTasks.length > 0 && (
<section>
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
🚫 Blocked ({blockedTasks.length})
</h2>
<div className="space-y-2">
{blockedTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onClick={() => setSelectedTask(task.id)}
/>
))}
</div>
</section>
)}
{/* Queue */}
<section>
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
📋 Queue ({queuedTasks.length})
</h2>
{queuedTasks.length === 0 ? (
<div className="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center text-gray-400">
Queue is empty
</div>
) : (
<div className="space-y-2">
{queuedTasks.map((task, i) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onMoveUp={() => handleMoveUp(i)}
onMoveDown={() => handleMoveDown(i)}
isFirst={i === 0}
isLast={i === queuedTasks.length - 1}
onClick={() => setSelectedTask(task.id)}
/>
))}
</div>
)}
</section>
{/* Completed */}
<section>
<button
onClick={() => setShowCompleted(!showCompleted)}
className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3 flex items-center gap-1 hover:text-gray-700"
>
{showCompleted ? "▾" : "▸"} Completed / Cancelled ({completedTasks.length})
</button>
{showCompleted && (
<div className="space-y-2 opacity-60">
{completedTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onStatusChange={handleStatusChange}
onClick={() => setSelectedTask(task.id)}
/>
))}
</div>
)}
</section>
</div>
{/* Task Detail Panel */}
{selectedTaskData && (
<TaskDetailPanel
task={selectedTaskData}
onClose={() => setSelectedTask(null)}
onStatusChange={(id, status) => {
handleStatusChange(id, status);
setSelectedTask(null);
}}
onTaskUpdated={refresh}
hasToken={isAuthenticated}
token=""
/>
)}
</div>
);
}