feat: breadcrumb nav, grouped properties sidebar, larger status selector, transitions

This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-07 22:34:31 +02:00
parent 8175b05b23
commit 6f2b0f39f7

View File

@@ -19,6 +19,7 @@ import type {
PreviewResult, PreviewResult,
UpdateResult, UpdateResult,
} from "@/lib/types"; } from "@/lib/types";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
@@ -85,7 +86,7 @@ function TransactionBubble({
return ( return (
<div className="flex justify-center py-2"> <div className="flex justify-center py-2">
<span className="text-xs text-[#8a8f98]"> <span className="text-xs text-muted-foreground">
{message} · {timeAgo} {message} · {timeAgo}
</span> </span>
</div> </div>
@@ -109,28 +110,28 @@ function TransactionBubble({
style={{ style={{
backgroundColor: isAgent backgroundColor: isAgent
? getInitialColor(tx.creator_id) ? getInitialColor(tx.creator_id)
: "#191a1b", : "var(--muted)",
}} }}
> >
<span <span
className="text-[11px] font-semibold" className="text-[11px] font-semibold"
style={{ color: isAgent ? "#f7f8f8" : "#8a8f98" }} style={{ color: isAgent ? "#f7f8f8" : "var(--muted-foreground)" }}
> >
{getInitial(tx.creator_id)} {getInitial(tx.creator_id)}
</span> </span>
</div> </div>
<div <div
className={cn( className={cn(
"max-w-[75%] rounded-lg px-3 py-2", "max-w-[75%] rounded-lg px-3 py-2 transition-all duration-150",
isAgent isAgent
? "bg-[#5e6ad2]/15 text-[#f7f8f8]" ? "bg-primary/15 text-foreground"
: isInternal : isInternal
? "bg-[#191a1b] border border-[rgba(255,255,255,0.05)] text-[#f7f8f8]" ? "bg-muted border border-border text-foreground"
: "bg-[#191a1b] text-[#f7f8f8]" : "bg-muted text-foreground"
)} )}
> >
{isInternal && ( {isInternal && (
<div className="text-[10px] font-semibold text-[#f59e0b] mb-0.5 uppercase tracking-wider"> <div className="text-[10px] font-semibold text-chart-3 mb-0.5 uppercase tracking-wider">
Internal note Internal note
</div> </div>
)} )}
@@ -139,7 +140,7 @@ function TransactionBubble({
? String((tx.data as Record<string, unknown>).body) ? String((tx.data as Record<string, unknown>).body)
: tx.transaction_type} : tx.transaction_type}
</p> </p>
<p className="text-[10px] text-[#8a8f98] mt-1">{timeAgo}</p> <p className="text-[10px] text-muted-foreground mt-1">{timeAgo}</p>
</div> </div>
</div> </div>
); );
@@ -262,17 +263,17 @@ export default function TicketDetailPage({
<div className="flex-1 p-4 space-y-4"> <div className="flex-1 p-4 space-y-4">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="flex gap-3"> <div key={i} className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-[#191a1b] animate-pulse" /> <div className="w-6 h-6 rounded-full bg-muted animate-pulse" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 bg-[#191a1b] rounded animate-pulse w-3/4" /> <div className="h-3 bg-muted rounded animate-pulse w-3/4" />
<div className="h-3 bg-[#191a1b] rounded animate-pulse w-1/2" /> <div className="h-3 bg-muted rounded animate-pulse w-1/2" />
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="w-80 bg-[#0f1011] border-l border-[rgba(255,255,255,0.05)] p-4 space-y-3"> <div className="w-80 bg-sidebar border-l border-border p-4 space-y-3">
{Array.from({ length: 5 }).map((_, i) => ( {Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-6 bg-[#191a1b] rounded animate-pulse" /> <div key={i} className="h-6 bg-muted rounded animate-pulse" />
))} ))}
</div> </div>
</div> </div>
@@ -282,10 +283,10 @@ export default function TicketDetailPage({
if (error && !ticket) { if (error && !ticket) {
return ( return (
<div className="flex flex-col items-center justify-center h-full"> <div className="flex flex-col items-center justify-center h-full">
<p className="text-red-400 text-sm">{error}</p> <p className="text-destructive text-sm">{error}</p>
<button <button
onClick={fetchData} onClick={fetchData}
className="mt-2 text-sm text-[#7170ff] hover:text-[#828fff]" className="mt-2 text-sm text-primary hover:text-primary/80 transition-all duration-150"
> >
Retry Retry
</button> </button>
@@ -296,7 +297,7 @@ export default function TicketDetailPage({
if (!ticket) { if (!ticket) {
return ( return (
<div className="flex flex-col items-center justify-center h-full"> <div className="flex flex-col items-center justify-center h-full">
<p className="text-[#8a8f98] text-sm">Ticket not found</p> <p className="text-muted-foreground text-sm">Ticket not found</p>
</div> </div>
); );
} }
@@ -311,28 +312,31 @@ export default function TicketDetailPage({
{/* Left panel — conversation */} {/* Left panel — conversation */}
<div className="flex-1 flex flex-col min-w-0"> <div className="flex-1 flex flex-col min-w-0">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-[rgba(255,255,255,0.05)]"> <div className="flex items-center gap-3 px-4 py-2.5 border-b border-border">
<Link <Link
href="/" href="/"
className="w-7 h-7 flex items-center justify-center rounded-md hover:bg-[rgba(255,255,255,0.05)] text-[#8a8f98] hover:text-[#d0d6e0] transition-colors flex-shrink-0" className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-all duration-150"
> >
<ArrowLeftIcon className="w-4 h-4" /> <ArrowLeftIcon className="w-3.5 h-3.5" />
All tickets
</Link> </Link>
<div className="flex-1 min-w-0"> </div>
<h1 className="text-sm font-semibold text-[#f7f8f8] truncate">
{ticket.subject} {/* Title */}
</h1> <div className="px-4 py-3 border-b border-border">
<p className="text-xs text-[#8a8f98]"> <h1 className="text-sm font-semibold text-foreground truncate">
{ticket.id.slice(0, 8)} · {queue?.name || ticket.queue_id} {ticket.subject}
</p> </h1>
</div> <p className="text-xs text-muted-foreground mt-0.5">
<span className="font-mono">{ticket.id.slice(0, 8)}</span> · {queue?.name || ticket.queue_id}
</p>
</div> </div>
{/* Conversation */} {/* Conversation */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
{transactions.length === 0 && ( {transactions.length === 0 && (
<div className="flex flex-col items-center justify-center py-20"> <div className="flex flex-col items-center justify-center py-20">
<p className="text-sm text-[#8a8f98]"> <p className="text-sm text-muted-foreground">
No activity yet No activity yet
</p> </p>
</div> </div>
@@ -343,16 +347,16 @@ export default function TicketDetailPage({
</div> </div>
{/* Reply box */} {/* Reply box */}
<div className="border-t border-[rgba(255,255,255,0.05)] bg-[#0f1011] p-3"> <div className="border-t border-border bg-sidebar p-3">
{/* Toggle tabs */} {/* Toggle tabs */}
<div className="flex gap-0.5 mb-2 p-0.5 rounded-lg bg-[#08090a] w-fit"> <div className="flex gap-0.5 mb-2 p-0.5 rounded-lg bg-background w-fit">
<button <button
onClick={() => setReplyMode("public")} onClick={() => setReplyMode("public")}
className={cn( className={cn(
"px-2.5 py-1 rounded-md text-xs font-medium transition-colors", "px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-150",
replyMode === "public" replyMode === "public"
? "bg-[#5e6ad2] text-[#f7f8f8]" ? "bg-primary text-primary-foreground"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-muted-foreground hover:text-foreground"
)} )}
> >
Reply Reply
@@ -360,10 +364,10 @@ export default function TicketDetailPage({
<button <button
onClick={() => setReplyMode("internal")} onClick={() => setReplyMode("internal")}
className={cn( className={cn(
"px-2.5 py-1 rounded-md text-xs font-medium transition-colors", "px-2.5 py-1 rounded-md text-xs font-medium transition-all duration-150",
replyMode === "internal" replyMode === "internal"
? "bg-[#5e6ad2] text-[#f7f8f8]" ? "bg-primary text-primary-foreground"
: "text-[#8a8f98] hover:text-[#d0d6e0]" : "text-muted-foreground hover:text-foreground"
)} )}
> >
Internal note Internal note
@@ -376,19 +380,19 @@ export default function TicketDetailPage({
onChange={(e) => setReplyText(e.target.value)} onChange={(e) => setReplyText(e.target.value)}
placeholder="Reply to this ticket..." placeholder="Reply to this ticket..."
rows={2} rows={2}
className="flex-1 px-3 py-2 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)] text-sm text-[#f7f8f8] placeholder:text-[#8a8f98] outline-none focus:border-[#5e6ad2] focus:ring-1 focus:ring-[#5e6ad2] resize-none" className="flex-1 px-3 py-2 rounded-lg bg-background border border-border text-sm text-foreground placeholder:text-muted-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary resize-none transition-all duration-150"
/> />
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<button className="w-8 h-8 flex items-center justify-center rounded-lg text-[#8a8f98] hover:text-[#d0d6e0] hover:bg-[rgba(255,255,255,0.05)] transition-colors" title="Attach file (coming soon)"> <button className="w-8 h-8 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-all duration-150" title="Attach file (coming soon)">
<PaperclipIcon className="w-4 h-4" /> <PaperclipIcon className="w-4 h-4" />
</button> </button>
<button <button
disabled={!replyText.trim()} disabled={!replyText.trim()}
className={cn( className={cn(
"w-8 h-8 flex items-center justify-center rounded-lg transition-colors", "w-8 h-8 flex items-center justify-center rounded-lg transition-all duration-150",
replyText.trim() replyText.trim()
? "bg-[#5e6ad2] text-[#f7f8f8] hover:bg-[#7170ff]" ? "bg-primary text-primary-foreground hover:bg-primary/80"
: "bg-[#191a1b] text-[#8a8f98] cursor-not-allowed" : "bg-muted text-muted-foreground cursor-not-allowed"
)} )}
> >
<SendIcon className="w-4 h-4" /> <SendIcon className="w-4 h-4" />
@@ -399,27 +403,27 @@ export default function TicketDetailPage({
</div> </div>
{/* Right panel — properties */} {/* Right panel — properties */}
<div className="w-80 flex-shrink-0 bg-[#0f1011] border-l border-[rgba(255,255,255,0.05)] flex flex-col overflow-y-auto"> <div className="w-80 flex-shrink-0 bg-sidebar border-l border-border flex flex-col overflow-y-auto">
<div className="p-4 space-y-4"> <div className="p-4 space-y-5">
{/* Status */} {/* Section: Status */}
<div> <div>
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider"> <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Status Status
</label> </h3>
<div className="relative"> <div className="relative">
<button <button
onClick={() => setStatusSelectOpen(!statusSelectOpen)} onClick={() => setStatusSelectOpen(!statusSelectOpen)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a] text-sm hover:border-[rgba(255,255,255,0.15)] transition-colors" className="w-full flex items-center gap-3 px-4 py-2.5 rounded-lg border border-border bg-background text-sm hover:border-foreground/20 transition-all duration-150"
> >
<span <span
className="w-2.5 h-2.5 rounded-full flex-shrink-0" className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: currentStatusColor }} style={{ backgroundColor: currentStatusColor }}
/> />
<span className="text-[#f7f8f8] font-medium flex-1 text-left"> <span className="text-foreground font-medium flex-1 text-left">
{currentStatusLabel} {currentStatusLabel}
</span> </span>
<svg <svg
className="w-4 h-4 text-[#8a8f98]" className="w-4 h-4 text-muted-foreground"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -434,7 +438,7 @@ export default function TicketDetailPage({
</button> </button>
{statusSelectOpen && ( {statusSelectOpen && (
<div className="absolute top-full left-0 right-0 mt-1 z-10 bg-[#191a1b] border border-[rgba(255,255,255,0.08)] rounded-lg shadow-xl overflow-hidden"> <div className="absolute top-full left-0 right-0 mt-1 z-10 bg-popover border border-border rounded-lg shadow-xl overflow-hidden">
{ALL_STATUSES.map((status) => { {ALL_STATUSES.map((status) => {
const color = STATUS_COLORS[status]; const color = STATUS_COLORS[status];
const label = STATUS_LABELS[status]; const label = STATUS_LABELS[status];
@@ -445,19 +449,19 @@ export default function TicketDetailPage({
onClick={() => handleStatusSelect(status)} onClick={() => handleStatusSelect(status)}
disabled={isCurrent} disabled={isCurrent}
className={cn( className={cn(
"w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors", "w-full flex items-center gap-3 px-4 py-2.5 text-sm text-left transition-all duration-150",
isCurrent isCurrent
? "bg-[rgba(255,255,255,0.03)] text-[#8a8f98] cursor-default" ? "bg-accent text-muted-foreground cursor-default"
: "text-[#d0d6e0] hover:bg-[rgba(255,255,255,0.05)]" : "text-foreground hover:bg-accent"
)} )}
> >
<span <span
className="w-2.5 h-2.5 rounded-full flex-shrink-0" className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />
{label} {label}
{isCurrent && ( {isCurrent && (
<span className="text-xs text-[#8a8f98] ml-auto"> <span className="text-xs text-muted-foreground ml-auto">
current current
</span> </span>
)} )}
@@ -471,10 +475,10 @@ export default function TicketDetailPage({
{/* Status change preview */} {/* Status change preview */}
{preview && ( {preview && (
<div className="p-3 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)]"> <div className="p-3 rounded-lg bg-background border border-border">
<p className="text-xs text-[#8a8f98] mb-2"> <p className="text-xs text-muted-foreground mb-2">
Preview: changing to{" "} Preview: changing to{" "}
<span className="text-[#f7f8f8] font-medium"> <span className="text-foreground font-medium">
{STATUS_LABELS[pendingStatus || ""]} {STATUS_LABELS[pendingStatus || ""]}
</span> </span>
</p> </p>
@@ -483,15 +487,15 @@ export default function TicketDetailPage({
{preview.prepared_scrips.map((scrip) => ( {preview.prepared_scrips.map((scrip) => (
<div <div
key={scrip.scripId} key={scrip.scripId}
className="text-xs text-[#d0d6e0] flex items-center gap-1.5" className="text-xs text-foreground flex items-center gap-1.5"
> >
<span className="w-2 h-2 rounded-full bg-[#f59e0b] flex-shrink-0" /> <span className="w-2 h-2 rounded-full bg-chart-3 flex-shrink-0" />
{scrip.scripName} {scrip.scripName}
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<p className="text-xs text-[#8a8f98] mb-3"> <p className="text-xs text-muted-foreground mb-3">
No scrips will fire No scrips will fire
</p> </p>
)} )}
@@ -499,7 +503,7 @@ export default function TicketDetailPage({
<button <button
onClick={handleApplyStatus} onClick={handleApplyStatus}
disabled={applyLoading} disabled={applyLoading}
className="px-2.5 py-1 rounded-md text-xs font-medium bg-[#5e6ad2] hover:bg-[#7170ff] text-[#f7f8f8] disabled:opacity-50 transition-colors" className="px-2.5 py-1 rounded-md text-xs font-medium bg-primary hover:bg-primary/80 text-primary-foreground disabled:opacity-50 transition-all duration-150"
> >
{applyLoading {applyLoading
? "Applying..." ? "Applying..."
@@ -508,7 +512,7 @@ export default function TicketDetailPage({
<button <button
onClick={handleCancelStatus} onClick={handleCancelStatus}
disabled={applyLoading} disabled={applyLoading}
className="px-2.5 py-1 rounded-md text-xs font-medium text-[#8a8f98] hover:text-[#d0d6e0] transition-colors" className="px-2.5 py-1 rounded-md text-xs font-medium text-muted-foreground hover:text-foreground transition-all duration-150"
> >
Cancel Cancel
</button> </button>
@@ -517,27 +521,27 @@ export default function TicketDetailPage({
)} )}
{previewError && ( {previewError && (
<div className="p-2 rounded-lg bg-red-400/5 border border-red-400/10"> <div className="p-2 rounded-lg bg-destructive/5 border border-destructive/10">
<p className="text-xs text-red-400">{previewError}</p> <p className="text-xs text-destructive">{previewError}</p>
</div> </div>
)} )}
{scripResults && ( {scripResults && (
<div className="p-3 rounded-lg bg-[#08090a] border border-[rgba(255,255,255,0.08)]"> <div className="p-3 rounded-lg bg-background border border-border">
<p className="text-xs text-[#8a8f98] mb-2">Scrip results:</p> <p className="text-xs text-muted-foreground mb-2">Scrip results:</p>
<div className="space-y-1"> <div className="space-y-1">
{scripResults.map((result) => ( {scripResults.map((result) => (
<div <div
key={result.scripId} key={result.scripId}
className={cn( className={cn(
"text-xs flex items-center gap-1.5", "text-xs flex items-center gap-1.5",
result.success ? "text-[#22c55e]" : "text-red-400" result.success ? "text-[#22c55e]" : "text-destructive"
)} )}
> >
<span <span
className={cn( className={cn(
"w-2 h-2 rounded-full flex-shrink-0", "w-2 h-2 rounded-full flex-shrink-0",
result.success ? "bg-[#22c55e]" : "bg-red-400" result.success ? "bg-[#22c55e]" : "bg-destructive"
)} )}
/> />
{result.message} {result.message}
@@ -546,119 +550,134 @@ export default function TicketDetailPage({
</div> </div>
<button <button
onClick={() => setScripResults(null)} onClick={() => setScripResults(null)}
className="mt-2 text-xs text-[#8a8f98] hover:text-[#d0d6e0]" className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-all duration-150"
> >
Dismiss Dismiss
</button> </button>
</div> </div>
)} )}
{/* Priority (placeholder) */} <Separator />
<div>
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
Priority
</label>
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a] text-sm text-[#8a8f98]">
Not set
</div>
</div>
{/* Assignee */} {/* Section: Assignment */}
<div> <div>
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider"> <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Assignee Assignment
</label> </h3>
{ticket.owner_id ? (
<div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a]">
<div
className="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0"
style={{
backgroundColor: getInitialColor(ticket.owner_id),
}}
>
<span className="text-[10px] font-semibold text-[#f7f8f8]">
{getInitial(ticket.owner_id)}
</span>
</div>
<span className="text-sm text-[#f7f8f8]">
{ticket.owner_id}
</span>
</div>
) : (
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a] text-sm text-[#8a8f98]">
Unassigned
</div>
)}
</div>
{/* Queue */} {/* Assignee */}
<div> <div className="mb-3">
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider"> <label className="block text-[11px] font-medium text-muted-foreground mb-1">
Queue Assignee
</label>
<div className="px-3 py-2 rounded-lg border border-[rgba(255,255,255,0.08)] bg-[#08090a]">
<span className="text-sm text-[#f7f8f8]">
{queue?.name || ticket.queue_id}
</span>
</div>
</div>
{/* Custom fields */}
{ticket.custom_fields && ticket.custom_fields.length > 0 && (
<div>
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider">
Custom fields
</label> </label>
<div className="space-y-1.5"> {ticket.owner_id ? (
{ticket.custom_fields.map((cf) => ( <div className="flex items-center gap-2 px-3 py-2 rounded-lg border border-border bg-background transition-all duration-150 hover:border-foreground/20">
<div <div
key={cf.id} className="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0"
className="flex justify-between items-center px-3 py-1.5 rounded-lg border border-[rgba(255,255,255,0.05)] bg-[#08090a]" style={{
backgroundColor: getInitialColor(ticket.owner_id),
}}
> >
<span className="text-xs text-[#8a8f98]"> <span className="text-[10px] font-semibold text-primary-foreground">
{cf.custom_field?.name || cf.custom_field_id} {getInitial(ticket.owner_id)}
</span>
<span className="text-xs text-[#f7f8f8] font-medium">
{cf.value}
</span> </span>
</div> </div>
))} <span className="text-sm text-foreground">
{ticket.owner_id}
</span>
</div>
) : (
<div className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-muted-foreground">
Unassigned
</div>
)}
</div>
{/* Priority (placeholder) */}
<div>
<label className="block text-[11px] font-medium text-muted-foreground mb-1">
Priority
</label>
<div className="px-3 py-2 rounded-lg border border-border bg-background text-sm text-muted-foreground">
Not set
</div> </div>
</div> </div>
)} </div>
{/* Dates */} <Separator />
{/* Section: Details */}
<div> <div>
<label className="block text-xs font-medium text-[#8a8f98] mb-1.5 uppercase tracking-wider"> <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
Dates Details
</label> </h3>
<div className="space-y-1 text-xs">
<div className="flex justify-between px-1"> {/* Queue */}
<span className="text-[#8a8f98]">Created</span> <div className="mb-3">
<span className="text-[#d0d6e0] tabular-nums"> <label className="block text-[11px] font-medium text-muted-foreground mb-1">
{formatDistanceToNow(new Date(ticket.created_at), { Queue
addSuffix: true, </label>
})} <div className="px-3 py-2 rounded-lg border border-border bg-background">
<span className="text-sm text-foreground">
{queue?.name || ticket.queue_id}
</span> </span>
</div> </div>
<div className="flex justify-between px-1"> </div>
<span className="text-[#8a8f98]">Updated</span>
<span className="text-[#d0d6e0] tabular-nums"> {/* Custom fields */}
{formatDistanceToNow(new Date(ticket.updated_at), { {ticket.custom_fields && ticket.custom_fields.length > 0 && (
addSuffix: true, <div className="mb-3">
})} <label className="block text-[11px] font-medium text-muted-foreground mb-1">
</span> Custom fields
</label>
<div className="space-y-1.5">
{ticket.custom_fields.map((cf) => (
<div
key={cf.id}
className="flex justify-between items-center px-3 py-1.5 rounded-lg border border-border bg-background"
>
<span className="text-xs text-muted-foreground">
{cf.custom_field?.name || cf.custom_field_id}
</span>
<span className="text-xs text-foreground font-medium">
{cf.value}
</span>
</div>
))}
</div>
</div> </div>
{ticket.resolved_at && ( )}
<div className="flex justify-between px-1">
<span className="text-[#8a8f98]">Resolved</span> {/* Dates */}
<span className="text-[#d0d6e0] tabular-nums"> <div>
{formatDistanceToNow(new Date(ticket.resolved_at), { <div className="space-y-1.5 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Created</span>
<span className="text-foreground tabular-nums">
{formatDistanceToNow(new Date(ticket.created_at), {
addSuffix: true, addSuffix: true,
})} })}
</span> </span>
</div> </div>
)} <div className="flex justify-between">
<span className="text-muted-foreground">Updated</span>
<span className="text-foreground tabular-nums">
{formatDistanceToNow(new Date(ticket.updated_at), {
addSuffix: true,
})}
</span>
</div>
{ticket.resolved_at && (
<div className="flex justify-between">
<span className="text-muted-foreground">Resolved</span>
<span className="text-foreground tabular-nums">
{formatDistanceToNow(new Date(ticket.resolved_at), {
addSuffix: true,
})}
</span>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>