because I got bored of customising my CV for every job
1import { useState } from "react";
2import { Badge, Card, StatusDot } from "@cv/ui";
3import type { QueueMessagesQuery } from "@/generated/graphql";
4
5type MessageConnection = QueueMessagesQuery["queueMessages"];
6type Message = MessageConnection["edges"][number]["node"];
7
8type StatusFilter = "all" | "pending" | "scheduled" | "processing";
9
10type DotColor = "green" | "yellow" | "red" | "gray";
11
12const statusColor: Record<string, DotColor> = {
13 pending: "yellow",
14 scheduled: "gray",
15 processing: "green",
16};
17
18const statusBadgeColor: Record<StatusFilter, string> = {
19 all: "ctp-blue",
20 pending: "ctp-yellow",
21 scheduled: "ctp-gray",
22 processing: "ctp-green",
23};
24
25const formatRelativeTime = (isoString: string): string => {
26 const diff = Date.now() - new Date(isoString).getTime();
27 const seconds = Math.floor(diff / 1000);
28
29 if (seconds < 60) return `${seconds}s ago`;
30 const minutes = Math.floor(seconds / 60);
31 if (minutes < 60) return `${minutes}m ago`;
32 const hours = Math.floor(minutes / 60);
33 if (hours < 24) return `${hours}h ago`;
34 return `${Math.floor(hours / 24)}d ago`;
35};
36
37interface QueueMessagesTableProps {
38 connection: MessageConnection;
39}
40
41export const QueueMessagesTable = ({ connection }: QueueMessagesTableProps) => {
42 const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
43
44 const messages = connection.edges.map((e) => e.node);
45
46 const filtered =
47 statusFilter === "all"
48 ? messages
49 : messages.filter((m) => m.status === statusFilter);
50
51 const statusOptions: StatusFilter[] = [
52 "all",
53 "pending",
54 "scheduled",
55 "processing",
56 ];
57
58 return (
59 <Card>
60 <div className="flex items-center justify-between mb-3">
61 <div>
62 <h3 className="text-lg font-medium text-ctp-text">Queue Messages</h3>
63 {connection.totalCount > 0 && (
64 <p className="text-xs text-ctp-subtext0 mt-0.5">
65 {connection.totalCount} messages
66 </p>
67 )}
68 </div>
69 </div>
70
71 {messages.length === 0 ? (
72 <p className="text-sm text-ctp-subtext0">No messages in queue</p>
73 ) : (
74 <>
75 <div className="mb-3 flex gap-1">
76 {statusOptions.map((s) => (
77 <button
78 key={s}
79 type="button"
80 onClick={() => setStatusFilter(s)}
81 className="focus:outline-none"
82 >
83 <Badge
84 color={
85 statusFilter === s
86 ? (statusBadgeColor[s] as "ctp-blue")
87 : "ctp-gray"
88 }
89 >
90 {s}
91 </Badge>
92 </button>
93 ))}
94 </div>
95 <div className="overflow-x-auto">
96 <table className="w-full text-sm">
97 <thead>
98 <tr className="border-b border-ctp-surface1 text-left text-ctp-subtext0">
99 <th className="pb-2 pr-4 font-medium">Time</th>
100 <th className="pb-2 pr-4 font-medium">Message</th>
101 <th className="pb-2 pr-4 font-medium">Queue</th>
102 <th className="pb-2 pr-4 font-medium">Status</th>
103 </tr>
104 </thead>
105 <tbody>
106 {filtered.map((msg) => (
107 <tr
108 key={msg.id}
109 className="border-b border-ctp-surface1 last:border-b-0"
110 >
111 <td className="py-2 pr-4 text-ctp-subtext0 whitespace-nowrap">
112 {formatRelativeTime(msg.createdAt)}
113 </td>
114 <td className="py-2 pr-4 text-ctp-text font-mono text-xs">
115 {msg.messageName ?? "-"}
116 </td>
117 <td className="py-2 pr-4 text-ctp-text">
118 {msg.queueName}
119 </td>
120 <td className="py-2 pr-4">
121 <span className="flex items-center gap-2">
122 <StatusDot
123 color={statusColor[msg.status] ?? "gray"}
124 />
125 {msg.status}
126 </span>
127 </td>
128 </tr>
129 ))}
130 </tbody>
131 </table>
132 {filtered.length === 0 && (
133 <p className="text-sm text-ctp-subtext0 text-center py-4">
134 No messages match the current filter
135 </p>
136 )}
137 </div>
138 </>
139 )}
140 </Card>
141 );
142};