From da7a45e15dbd202e50982450ea1273e031f7d2be Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sat, 21 Mar 2026 14:18:31 +0800 Subject: [PATCH] feat(web): implement Issue detail page with properties and activity - Left column: breadcrumb, title, description, activity timeline - Right column: properties sidebar (status, priority, assignee, due date) - Activity merges status changes and comments in chronological order - Agent comments/actions distinguished with purple badge - Comment input placeholder at bottom Co-Authored-By: Claude Opus 4.6 --- apps/web/app/(dashboard)/issues/[id]/page.tsx | 284 +++++++++++++++++- 1 file changed, 281 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(dashboard)/issues/[id]/page.tsx b/apps/web/app/(dashboard)/issues/[id]/page.tsx index b6eb92f9..47f615f6 100644 --- a/apps/web/app/(dashboard)/issues/[id]/page.tsx +++ b/apps/web/app/(dashboard)/issues/[id]/page.tsx @@ -1,12 +1,290 @@ +"use client"; + +import { use } from "react"; +import Link from "next/link"; +import { + ArrowLeft, + Bot, + Calendar, + ChevronRight, + Circle, + User, +} from "lucide-react"; +import { + MOCK_ISSUES, + STATUS_CONFIG, + PRIORITY_CONFIG, +} from "../_data/mock"; +import type { MockAssignee } from "../_data/mock"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function formatDate(date: string | null): string { + if (!date) return "—"; + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +function ActorBadge({ actor }: { actor: MockAssignee }) { + return ( + + {actor.type === "agent" && } + {actor.name} + + ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + export default function IssueDetailPage({ params, }: { params: Promise<{ id: string }>; }) { + const { id } = use(params); + const issue = MOCK_ISSUES.find((i) => i.id === id); + + if (!issue) { + return ( +
+ Issue not found +
+ ); + } + + const statusCfg = STATUS_CONFIG[issue.status]; + const priorityCfg = PRIORITY_CONFIG[issue.priority]; + const isOverdue = + issue.dueDate && new Date(issue.dueDate) < new Date() && issue.status !== "done"; + + // Merge comments + activity into a single timeline sorted by time + const timeline = [ + ...issue.activity.map((a) => ({ + id: a.id, + type: "activity" as const, + actor: a.actor, + content: a.action, + createdAt: a.createdAt, + })), + ...issue.comments.map((c) => ({ + id: c.id, + type: "comment" as const, + actor: c.author, + content: c.body, + createdAt: c.createdAt, + })), + ].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + return ( -
-

Issue Detail

-

Issue detail view

+
+ {/* Left column — content */} +
+ {/* Breadcrumb */} +
+ + + Issues + + + {issue.key} +
+ +
+ {/* Title */} +

{issue.title}

+ + {/* Description */} + {issue.description && ( +
+ {issue.description} +
+ )} + + {/* Activity section */} +
+

Activity

+
+ {timeline.map((entry) => + entry.type === "comment" ? ( +
+
+ {entry.actor.type === "agent" ? ( + + ) : ( + entry.actor.avatar.charAt(0) + )} +
+
+
+ + + {timeAgo(entry.createdAt)} + +
+
+ {entry.content} +
+
+
+ ) : ( +
+
+ +
+ + {entry.content} + {timeAgo(entry.createdAt)} +
+ ) + )} + + {/* Comment input placeholder */} +
+
+ +
+
+ Leave a comment... +
+
+
+
+
+
+ + {/* Right column — properties sidebar */} +
+
+ {/* Status */} +
+
Status
+
+ + + {statusCfg.label} + +
+
+ + {/* Priority */} +
+
Priority
+
+ + {priorityCfg.shortLabel} + + {priorityCfg.label} +
+
+ + {/* Assignee */} +
+
Assignee
+ {issue.assignee ? ( +
+
+ {issue.assignee.type === "agent" ? ( + + ) : ( + issue.assignee.avatar.charAt(0) + )} +
+ {issue.assignee.name} + {issue.assignee.type === "agent" && ( + Agent + )} +
+ ) : ( + Unassigned + )} +
+ + {/* Due Date */} +
+
Due Date
+
+ + + {formatDate(issue.dueDate)} + +
+
+ + {/* Creator */} +
+
Created by
+
+
+ {issue.creator.type === "agent" ? ( + + ) : ( + issue.creator.avatar.charAt(0) + )} +
+ {issue.creator.name} +
+
+ + {/* Dates */} +
+
Created
+ {formatDate(issue.createdAt)} +
+
+
Updated
+ {formatDate(issue.updatedAt)} +
+
+
); }