feat(issues): rewrite issues page with component decomposition and persisted filters
- Decompose monolithic 472-line page.tsx into focused components: board-card, board-column, board-view, list-row, list-view, issues-header, issues-page - Add view-store with Zustand persist for viewMode and multi-select status/priority filters - Fix kanban DnD with pointerWithin + closestCenter collision detection - Add workspace breadcrumb header and Linear-style filter dropdowns using DropdownMenu with CheckboxItem for multi-select - Status filter hides kanban columns, priority filter hides cards - Drop target highlight with bg-accent Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
655aa40732
commit
586a4916d1
11 changed files with 650 additions and 467 deletions
79
apps/web/features/issues/components/board-card.tsx
Normal file
79
apps/web/features/issues/components/board-card.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import type { Issue } from "@multica/types";
|
||||
import { ActorAvatar } from "@/components/common/actor-avatar";
|
||||
import { PriorityIcon } from "./priority-icon";
|
||||
|
||||
function formatDate(date: string): string {
|
||||
return new Date(date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
export function BoardCardContent({ issue }: { issue: Issue }) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<PriorityIcon priority={issue.priority} />
|
||||
<span>{issue.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
<p className="mt-1.5 text-sm leading-snug">{issue.title}</p>
|
||||
<div className="mt-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{issue.assignee_type && issue.assignee_id && (
|
||||
<ActorAvatar
|
||||
actorType={issue.assignee_type}
|
||||
actorId={issue.assignee_id}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{issue.due_date && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatDate(issue.due_date)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DraggableBoardCard({ issue }: { issue: Issue }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: issue.id,
|
||||
data: { status: issue.status },
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={isDragging ? "opacity-30" : ""}
|
||||
>
|
||||
<Link
|
||||
href={`/issues/${issue.id}`}
|
||||
className={`block transition-colors hover:opacity-80 ${isDragging ? "pointer-events-none" : ""}`}
|
||||
>
|
||||
<BoardCardContent issue={issue} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue