because I got bored of customising my CV for every job
at main 263 lines 8.6 kB view raw
1import { Button, Card, TextInput } from "@cv/ui"; 2import { keepPreviousData, useQueryClient } from "@tanstack/react-query"; 3import { useState } from "react"; 4import { 5 useAdminCreateLookupEntityMutation, 6 useAdminDeleteLookupEntityMutation, 7 useAdminLookupEntitiesQuery, 8 useAdminUpdateLookupEntityMutation, 9} from "@/generated/graphql"; 10 11interface AdminLookupTableProps { 12 entityType: string; 13 title: string; 14} 15 16type LookupEntity = { 17 id: string; 18 name: string; 19 description: string | null; 20 createdAt: string; 21 updatedAt: string; 22}; 23 24const formatDate = (iso: string) => 25 new Date(iso).toLocaleDateString(undefined, { 26 year: "numeric", 27 month: "short", 28 day: "numeric", 29 }); 30 31export const AdminLookupTable = ({ 32 entityType, 33 title, 34}: AdminLookupTableProps) => { 35 const queryClient = useQueryClient(); 36 const [searchTerm, setSearchTerm] = useState(""); 37 const [newName, setNewName] = useState(""); 38 const [newDescription, setNewDescription] = useState(""); 39 const [editingId, setEditingId] = useState<string | null>(null); 40 const [editName, setEditName] = useState(""); 41 const [editDescription, setEditDescription] = useState(""); 42 const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null); 43 44 const queryKey = ["AdminLookupEntities", { entityType, searchTerm }]; 45 46 const { data, isLoading } = useAdminLookupEntitiesQuery( 47 { 48 entityType: entityType as never, 49 searchTerm: searchTerm || undefined, 50 }, 51 { placeholderData: keepPreviousData }, 52 ); 53 54 const invalidate = () => 55 queryClient.invalidateQueries({ queryKey: ["AdminLookupEntities"] }); 56 57 const createMutation = useAdminCreateLookupEntityMutation({ 58 onSuccess: () => { 59 invalidate(); 60 setNewName(""); 61 setNewDescription(""); 62 }, 63 }); 64 65 const updateMutation = useAdminUpdateLookupEntityMutation({ 66 onSuccess: () => { 67 invalidate(); 68 setEditingId(null); 69 }, 70 }); 71 72 const deleteMutation = useAdminDeleteLookupEntityMutation({ 73 onSuccess: () => { 74 invalidate(); 75 setDeleteConfirmId(null); 76 }, 77 }); 78 79 const handleCreate = (e: React.FormEvent) => { 80 e.preventDefault(); 81 if (!newName.trim()) return; 82 createMutation.mutate({ 83 entityType: entityType as never, 84 name: newName.trim(), 85 description: newDescription.trim() || undefined, 86 }); 87 }; 88 89 const handleUpdate = (id: string) => { 90 updateMutation.mutate({ 91 entityType: entityType as never, 92 id, 93 name: editName.trim() || undefined, 94 description: editDescription, 95 }); 96 }; 97 98 const handleDelete = (id: string) => { 99 deleteMutation.mutate({ entityType: entityType as never, id }); 100 }; 101 102 const startEditing = (entity: LookupEntity) => { 103 setEditingId(entity.id); 104 setEditName(entity.name); 105 setEditDescription(entity.description ?? ""); 106 }; 107 108 const entities = (data?.adminLookupEntities ?? []) as LookupEntity[]; 109 110 return ( 111 <Card> 112 <div className="mb-4 flex items-center justify-between"> 113 <h3 className="text-lg font-medium text-ctp-text">{title}</h3> 114 <span className="text-xs text-ctp-subtext0"> 115 {entities.length} {entities.length === 1 ? "item" : "items"} 116 </span> 117 </div> 118 119 <div className="mb-4"> 120 <TextInput 121 label="" 122 placeholder="Search..." 123 value={searchTerm} 124 onChange={setSearchTerm} 125 /> 126 </div> 127 128 <form onSubmit={handleCreate} className="mb-4 flex gap-2"> 129 <TextInput 130 label="" 131 placeholder="Name" 132 value={newName} 133 onChange={setNewName} 134 /> 135 <TextInput 136 label="" 137 placeholder="Description (optional)" 138 value={newDescription} 139 onChange={setNewDescription} 140 /> 141 <Button type="submit" disabled={!newName.trim()}> 142 Add 143 </Button> 144 </form> 145 146 {isLoading ? ( 147 <p className="text-sm text-ctp-subtext0">Loading...</p> 148 ) : entities.length === 0 ? ( 149 <p className="text-sm text-ctp-subtext0">No entities found</p> 150 ) : ( 151 <div className="overflow-x-auto"> 152 <table className="w-full text-sm"> 153 <thead> 154 <tr className="border-b border-ctp-surface1 text-left text-ctp-subtext0"> 155 <th className="pb-2 pr-4 font-medium">Name</th> 156 <th className="pb-2 pr-4 font-medium">Description</th> 157 <th className="pb-2 pr-4 font-medium">Created</th> 158 <th className="pb-2 pr-4 font-medium text-right">Actions</th> 159 </tr> 160 </thead> 161 <tbody> 162 {entities.map((entity) => ( 163 <tr 164 key={entity.id} 165 className="border-b border-ctp-surface1 last:border-b-0" 166 > 167 {editingId === entity.id ? ( 168 <> 169 <td className="py-2 pr-4"> 170 <TextInput 171 label="" 172 value={editName} 173 onChange={setEditName} 174 /> 175 </td> 176 <td className="py-2 pr-4"> 177 <TextInput 178 label="" 179 value={editDescription} 180 onChange={setEditDescription} 181 /> 182 </td> 183 <td className="py-2 pr-4 text-ctp-subtext0"> 184 {formatDate(entity.createdAt)} 185 </td> 186 <td className="py-2 pr-4 text-right"> 187 <div className="flex justify-end gap-1"> 188 <Button 189 variant="ghost" 190 size="sm" 191 onClick={() => handleUpdate(entity.id)} 192 > 193 Save 194 </Button> 195 <Button 196 variant="ghost" 197 size="sm" 198 onClick={() => setEditingId(null)} 199 > 200 Cancel 201 </Button> 202 </div> 203 </td> 204 </> 205 ) : ( 206 <> 207 <td className="py-2 pr-4 text-ctp-text"> 208 {entity.name} 209 </td> 210 <td className="py-2 pr-4 text-ctp-subtext0"> 211 {entity.description ?? "-"} 212 </td> 213 <td className="py-2 pr-4 text-ctp-subtext0"> 214 {formatDate(entity.createdAt)} 215 </td> 216 <td className="py-2 pr-4 text-right"> 217 {deleteConfirmId === entity.id ? ( 218 <div className="flex justify-end gap-1"> 219 <Button 220 variant="ghost" 221 size="sm" 222 onClick={() => handleDelete(entity.id)} 223 > 224 Confirm 225 </Button> 226 <Button 227 variant="ghost" 228 size="sm" 229 onClick={() => setDeleteConfirmId(null)} 230 > 231 Cancel 232 </Button> 233 </div> 234 ) : ( 235 <div className="flex justify-end gap-1"> 236 <Button 237 variant="ghost" 238 size="sm" 239 onClick={() => startEditing(entity)} 240 > 241 Edit 242 </Button> 243 <Button 244 variant="ghost" 245 size="sm" 246 onClick={() => setDeleteConfirmId(entity.id)} 247 > 248 Delete 249 </Button> 250 </div> 251 )} 252 </td> 253 </> 254 )} 255 </tr> 256 ))} 257 </tbody> 258 </table> 259 </div> 260 )} 261 </Card> 262 ); 263};