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)}
+
+
+
);
}