1
0
Fork 0
polaris-task-force/src/components/frontend/helpdesk/TicketTimeline.tsx

113 lines
4.2 KiB
TypeScript

"use client";
import { RichText } from "@payloadcms/richtext-lexical/react";
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
import type { Ticket, User } from "@/payload-types";
import { cn } from "@/lib/utils";
import { HistoryIcon } from "lucide-react";
interface TicketTimelineProps {
readonly ticket: Pick<Ticket, "activity" | "reporter">;
readonly currentUserId: number;
}
type UserRelation = number | User | null | undefined;
function resolveUser(relation: UserRelation): User | null {
return typeof relation === "object" && relation ? relation : null;
}
function userId(relation: UserRelation): number | null {
return typeof relation === "object" && relation ? relation.id : (relation ?? null);
}
function displayName(relation: UserRelation, fallback: string): string {
const user = resolveUser(relation);
if (user) return user.payloadDisplayName || user.username || user.displayName;
if (typeof relation === "number") return `User #${relation}`;
return fallback;
}
function formatDate(iso: string | null | undefined): string {
if (!iso) return "Unknown time";
return new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(iso));
}
export function TicketTimeline({ ticket, currentUserId }: TicketTimelineProps) {
const activities = [...(ticket.activity ?? [])].sort(
(left, right) =>
new Date(left.created_at ?? 0).getTime() - new Date(right.created_at ?? 0).getTime(),
);
const reporterName = displayName(ticket.reporter, "Reporter");
return (
<div className="flex flex-col gap-3">
{activities.length === 0 && (
<p className="py-4 text-center text-sm italic text-muted-foreground">No activity yet.</p>
)}
{activities.map((activity, index) => {
const key = activity.id ?? `${activity.type}-${activity.created_at ?? "unknown"}-${index}`;
const content = convertLexicalToPlaintext({ data: activity.content });
if (activity.type === "system") {
return (
<div
key={key}
className="flex flex-wrap items-center justify-center gap-2 px-2 py-2 text-center text-xs text-muted-foreground"
>
<span aria-hidden="true" className="size-1.5 rounded-full bg-muted-foreground/50" />
<HistoryIcon className="size-3.5" />
<span>{content || "Ticket activity updated"}</span>
<time className="font-mono text-[10px]" dateTime={activity.created_at ?? undefined}>
{formatDate(activity.created_at)}
</time>
</div>
);
}
const isClientMessage = activity.type === "client_message";
const authorId = userId(activity.author);
const authorName = displayName(
activity.author,
isClientMessage ? reporterName : "Staff member",
);
const bubbleLabel = isClientMessage
? authorId === currentUserId
? "You"
: reporterName
: "Staff";
return (
<div key={key} className={cn("flex", isClientMessage ? "justify-end" : "justify-start")}>
<div
className={cn(
"flex max-w-[min(85%,42rem)] flex-col gap-1 rounded-lg border px-3 py-2 text-sm",
isClientMessage
? "border-blue-500/30 bg-blue-500/10"
: "border-amber-500/30 bg-amber-500/5",
)}
>
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{bubbleLabel}
</span>
<div
className={cn(
"prose prose-sm max-w-none prose-invert [&_p]:my-1 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal",
isClientMessage ? "text-blue-100/90" : "text-amber-100/90",
)}
>
<RichText data={activity.content} />
</div>
<div className="text-[10px] text-muted-foreground">
{authorName} · {formatDate(activity.created_at)}
</div>
</div>
</div>
);
})}
</div>
);
}