because I got bored of customising my CV for every job
at main 142 lines 4.6 kB view raw
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};