- TaskCard: hide action buttons on mobile (tap to open detail panel), smaller text/badges, word-break titles - TaskDetailPanel: full-screen on mobile with Back button, responsive padding, stacked timeline, hidden UUID on small screens - QueuePage: sticky header offset for mobile nav bar (top-14) - Priority/source grid stacks vertically on mobile
209 lines
7.0 KiB
TypeScript
209 lines
7.0 KiB
TypeScript
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-14 md:top-0 z-30">
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-3 sm:py-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-lg sm:text-xl font-bold text-gray-900">Task Queue</h1>
|
|
<p className="text-xs sm: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-3 sm:px-4 py-2 rounded-lg hover:bg-amber-600 transition font-medium"
|
|
>
|
|
+ New
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<CreateTaskModal
|
|
open={showCreate}
|
|
onClose={() => setShowCreate(false)}
|
|
onCreate={handleCreate}
|
|
/>
|
|
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 py-4 sm:py-6 space-y-5 sm: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>
|
|
);
|
|
}
|