hermes-kanban-miniapp/frontend/src/components/KanbanBoard.tsx
kisskin ade88808fb
Some checks failed
CI / backend (push) Failing after 37s
CI / frontend (push) Failing after 21s
feat: M4 docker deployment - bridge + frontend containers
- packages/bridge/Dockerfile: multi-stage build (node:22-alpine)
- frontend/Dockerfile: multi-stage build + nginx:alpine
- frontend/nginx.conf: proxy /bridge to bridge service
- docker-compose.yml: bridge (3456) + frontend (8080)
- .dockerignore: exclude backend/node_modules/dist
- Fix vite proxy target to port 3456
- .env with bridge vars (AUTH_MODE=dev)
2026-07-10 14:58:20 +03:00

89 lines
3.1 KiB
TypeScript

import type { TaskSummary, TaskStatus, ColumnDef } from '@kanban/contracts'
import TaskCard from './TaskCard'
interface Props {
tasks: TaskSummary[]
columns: ColumnDef[]
onTaskTap: (task: TaskSummary) => void
onRefresh: () => void
}
export default function KanbanBoard({ tasks, columns, onTaskTap, onRefresh }: Props) {
// Group tasks by status
const grouped: Record<TaskStatus, TaskSummary[]> = {} as Record<TaskStatus, TaskSummary[]>
for (const col of columns) {
grouped[col.status] = []
}
for (const task of tasks) {
const status = task.status as TaskStatus
if (!grouped[status]) {
grouped[status] = []
}
grouped[status].push(task)
}
return (
<div className="flex-1 overflow-x-auto">
<div className="flex gap-3 p-4 min-w-max pb-20">
{columns.map((col) => (
<div
key={col.status}
className="flex-shrink-0 w-[280px] flex flex-col"
>
{/* Column header */}
<div className="flex items-center justify-between mb-3 px-1">
<div className="flex items-center gap-2">
<StatusDot status={col.status} />
<h3 className="font-medium text-sm text-gray-300 uppercase tracking-wider">
{col.title}
</h3>
</div>
<span className="text-xs text-gray-500 bg-white/5 px-2 py-0.5 rounded-full">
{grouped[col.status]?.length || 0}
</span>
</div>
{/* Column content */}
<div className="flex-1 space-y-2 min-h-[200px]">
{grouped[col.status]?.map((task) => (
<TaskCard
key={task.id}
task={task}
onTap={() => onTaskTap(task)}
/>
))}
{(!grouped[col.status] || grouped[col.status].length === 0) && (
<div className="text-center py-8 text-gray-600 text-sm">
No tasks
</div>
)}
</div>
</div>
))}
</div>
{/* Pull to refresh hint */}
<button
onClick={onRefresh}
className="fixed bottom-4 right-4 p-3 bg-blue-600 rounded-full shadow-lg z-10"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
</svg>
</button>
</div>
)
}
function StatusDot({ status }: { status: TaskStatus }) {
const colors: Record<TaskStatus, string> = {
triage: 'bg-gray-400',
todo: 'bg-blue-400',
ready: 'bg-yellow-400',
running: 'bg-green-400',
blocked: 'bg-red-400',
done: 'bg-emerald-400',
archived: 'bg-gray-500',
}
return <div className={`w-2 h-2 rounded-full ${colors[status] || 'bg-gray-400'}`} />
}