learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
at main 209 lines 7.6 kB view raw
1import { createMemo, createSignal, For, Show, splitProps } from "solid-js"; 2import type { Accessor, Component, JSX } from "solid-js"; 3 4export type Column<T> = { 5 key: keyof T | string; 6 header: string; 7 sortable?: boolean; 8 render?: (row: T, index: number) => JSX.Element; 9 width?: string; 10}; 11 12type DataTableProps<T> = { 13 columns: Column<T>[]; 14 data: T[]; 15 getRowId: (row: T) => string; 16 selectable?: boolean; 17 expandable?: (row: T) => JSX.Element | null; 18 onSelectionChange?: (selectedIds: string[]) => void; 19 class?: string; 20}; 21 22type SortDirection = "asc" | "desc" | null; 23 24const SortIcon: Component<{ direction: SortDirection }> = (props) => ( 25 <svg class="w-4 h-4 ml-1 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 26 <Show when={props.direction === "asc"}> 27 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" /> 28 </Show> 29 <Show when={props.direction === "desc"}> 30 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> 31 </Show> 32 <Show when={!props.direction}> 33 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l4-4 4 4M8 15l4 4 4-4" /> 34 </Show> 35 </svg> 36); 37 38export function DataTable<T>(props: DataTableProps<T>): JSX.Element { 39 const [local, _others] = splitProps(props, [ 40 "columns", 41 "data", 42 "getRowId", 43 "selectable", 44 "expandable", 45 "onSelectionChange", 46 "class", 47 ]); 48 49 const [sortKey, setSortKey] = createSignal<string | null>(null); 50 const [sortDir, setSortDir] = createSignal<SortDirection>(null); 51 const [selected, setSelected] = createSignal<Set<string>>(new Set()); 52 const [expanded, setExpanded] = createSignal<Set<string>>(new Set()); 53 54 const sortedData: Accessor<T[]> = createMemo(() => { 55 const key = sortKey(); 56 const dir = sortDir(); 57 if (!key || !dir) return local.data; 58 59 return [...local.data].sort((a, b) => { 60 const aVal = (a as Record<string, unknown>)[key]; 61 const bVal = (b as Record<string, unknown>)[key]; 62 if (aVal === bVal) return 0; 63 if (aVal == null) return 1; 64 if (bVal == null) return -1; 65 const cmp = aVal < bVal ? -1 : 1; 66 return dir === "asc" ? cmp : -cmp; 67 }); 68 }); 69 70 const handleSort = (key: string) => { 71 if (sortKey() === key) { 72 setSortDir((d) => (d === "asc" ? "desc" : d === "desc" ? null : "asc")); 73 if (sortDir() === null) setSortKey(null); 74 } else { 75 setSortKey(key); 76 setSortDir("asc"); 77 } 78 }; 79 80 const toggleSelect = (id: string) => { 81 setSelected((prev) => { 82 const next = new Set(prev); 83 if (next.has(id)) next.delete(id); 84 else next.add(id); 85 local.onSelectionChange?.([...next]); 86 return next; 87 }); 88 }; 89 90 const toggleSelectAll = () => { 91 if (selected().size === local.data.length) { 92 setSelected(new Set<string>()); 93 local.onSelectionChange?.([]); 94 } else { 95 const all = new Set(local.data.map(local.getRowId)); 96 setSelected(all); 97 local.onSelectionChange?.([...all]); 98 } 99 }; 100 101 const toggleExpand = (id: string) => { 102 setExpanded((prev) => { 103 const next = new Set(prev); 104 if (next.has(id)) next.delete(id); 105 else next.add(id); 106 return next; 107 }); 108 }; 109 110 const getCellValue = (row: T, col: Column<T>, index: number): JSX.Element => { 111 if (col.render) return col.render(row, index); 112 const value = (row as Record<string, unknown>)[col.key as string]; 113 return <>{value != null ? String(value) : ""}</>; 114 }; 115 116 return ( 117 <div class={`overflow-x-auto ${local.class || ""}`}> 118 <table class="w-full text-sm text-left"> 119 <thead class="text-xs text-gray-400 uppercase bg-gray-900 border-b border-gray-700"> 120 <tr> 121 <Show when={local.expandable}> 122 <th class="w-8 px-2 py-3" /> 123 </Show> 124 <Show when={local.selectable}> 125 <th class="w-8 px-2 py-3"> 126 <input 127 type="checkbox" 128 checked={selected().size === local.data.length && local.data.length > 0} 129 onChange={toggleSelectAll} 130 class="w-4 h-4 rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500" /> 131 </th> 132 </Show> 133 <For each={local.columns}> 134 {(col) => ( 135 <th 136 class={`px-4 py-3 ${col.sortable ? "cursor-pointer hover:bg-gray-800 select-none" : ""}`} 137 style={{ width: col.width }} 138 onClick={() => col.sortable && handleSort(col.key as string)}> 139 <span class="flex items-center"> 140 {col.header} 141 <Show when={col.sortable}> 142 <SortIcon direction={sortKey() === col.key ? sortDir() : null} /> 143 </Show> 144 </span> 145 </th> 146 )} 147 </For> 148 </tr> 149 </thead> 150 <tbody> 151 <For each={sortedData()}> 152 {(row, index) => { 153 const id = local.getRowId(row); 154 const isExpanded = () => expanded().has(id); 155 const expandedContent = () => local.expandable?.(row); 156 157 return ( 158 <> 159 <tr class="border-b border-gray-800 hover:bg-gray-800/50 text-gray-300"> 160 <Show when={local.expandable}> 161 <td class="px-2 py-3"> 162 <Show when={expandedContent()}> 163 <button 164 onClick={() => toggleExpand(id)} 165 class="p-1 hover:bg-gray-700 rounded" 166 aria-expanded={isExpanded()} 167 aria-label={isExpanded() ? "Collapse row" : "Expand row"}> 168 <svg 169 class={`w-4 h-4 transition-transform ${isExpanded() ? "rotate-90" : ""}`} 170 fill="none" 171 viewBox="0 0 24 24" 172 stroke="currentColor"> 173 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> 174 </svg> 175 </button> 176 </Show> 177 </td> 178 </Show> 179 <Show when={local.selectable}> 180 <td class="px-2 py-3"> 181 <input 182 type="checkbox" 183 checked={selected().has(id)} 184 onChange={() => toggleSelect(id)} 185 class="w-4 h-4 rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500" /> 186 </td> 187 </Show> 188 <For each={local.columns}> 189 {(col) => <td class="px-4 py-3">{getCellValue(row, col, index())}</td>} 190 </For> 191 </tr> 192 <Show when={isExpanded() && expandedContent()}> 193 <tr class="bg-gray-900/50"> 194 <td 195 colSpan={local.columns.length + (local.selectable ? 1 : 0) + (local.expandable ? 1 : 0)} 196 class="px-4 py-3"> 197 {expandedContent()} 198 </td> 199 </tr> 200 </Show> 201 </> 202 ); 203 }} 204 </For> 205 </tbody> 206 </table> 207 </div> 208 ); 209}