extremely wip tangled spa
1import { useParams } from "@solidjs/router"; 2import { 3 createMemo, 4 createResource, 5 createSignal, 6 For, 7 Match, 8 onMount, 9 Show, 10 Suspense, 11 Switch, 12} from "solid-js"; 13import type { Commit, DID, DiffTextFragment } from "../../../util/types"; 14import { useDid } from "../context"; 15import { Header } from "../main"; 16import { getRepoCommit } from "../main.data"; 17import { buildTree, type TreeNode } from "./commit.data"; 18 19function RenderTree(props: { tree: TreeNode; skip?: boolean }) { 20 if (props.skip) 21 return ( 22 <For each={props.tree.children}> 23 {(node) => <RenderTree tree={node} />} 24 </For> 25 ); 26 const [displayChildren, setDisplayChildren] = createSignal(true); 27 return ( 28 <Switch> 29 <Match when={props.tree.type === "file"}> 30 <a 31 class="flex cursor-default select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700" 32 href={`#file-${props.tree.fullPath}`} 33 > 34 <div class="iconify gravity-ui--file" /> 35 <span class="select-text">{props.tree.name}</span> 36 </a> 37 </Match> 38 <Match when={props.tree.type === "directory"}> 39 <div 40 class="flex select-none flex-row items-center gap-1 rounded p-1 text-xs hover:bg-gray-100 hover:dark:bg-gray-700" 41 onclick={() => setDisplayChildren(!displayChildren())} 42 > 43 <div class="iconify gravity-ui--folder-fill" /> 44 <span class="select-text">{props.tree.name}</span> 45 </div> 46 <div 47 class={`ml-1 flex flex-col border-gray-200 border-l pl-1 dark:border-gray-700 ${displayChildren() ? "" : "hidden"}`} 48 > 49 <For each={props.tree.children}> 50 {(node) => <RenderTree tree={node} />} 51 </For> 52 </div> 53 </Match> 54 </Switch> 55 ); 56} 57 58function Fragment(props: { 59 file: string; 60 data: DiffTextFragment; 61 index: number; 62 numberSize: number; 63}) { 64 let lineNumber = props.data.NewPosition; 65 let iOld = props.data.OldPosition; 66 let iNew = props.data.NewPosition; 67 68 return ( 69 <Show when={!props.data.is_binary} fallback={<div>binary data</div>}> 70 <Show when={props.index !== 0}> 71 <div class="h-5 w-full select-none bg-gray-100 text-center font-mono text-gray-700 dark:bg-gray-700 dark:text-gray-300"> 72 ··· 73 </div> 74 </Show> 75 <div class="w-full whitespace-pre font-mono"> 76 <For each={props.data.Lines}> 77 {(line) => { 78 const lineNumberOld = line.Op === 2 ? "" : (iOld++).toString(); 79 const lineNumberNew = line.Op === 1 ? "" : (iNew++).toString(); 80 const fillerOld = " ".repeat( 81 props.numberSize - lineNumberOld.length, 82 ); 83 const fillerNew = " ".repeat( 84 props.numberSize - lineNumberNew.length, 85 ); 86 return ( 87 <Line 88 file={props.file} 89 index={props.index} 90 line={line} 91 lineNumber={lineNumber++} 92 lineNumberNew={lineNumberNew} 93 lineNumberOld={lineNumberOld} 94 fillerOld={fillerOld} 95 fillerNew={fillerNew} 96 /> 97 ); 98 }} 99 </For> 100 </div> 101 </Show> 102 ); 103} 104 105function Line(props: { 106 file: string; 107 index: number; 108 line: { Op: number; Line: string }; 109 lineNumber: number; 110 lineNumberOld: string; 111 lineNumberNew: string; 112 fillerOld: string; 113 fillerNew: string; 114}) { 115 const id = `line-${props.file}-${props.index}-${props.lineNumber.toString()}`; 116 return ( 117 <div 118 class="flex scroll-mt-10 flex-row text-gray-400 *:flex *:flex-row dark:text-gray-500" 119 id={id} 120 > 121 <div class="sticky left-0 select-none border-gray-200 border-r bg-white px-1 *:flex dark:border-gray-700 dark:bg-gray-800"> 122 <span class="float-right mr-1 w-1/2 justify-end"> 123 <span>{props.fillerOld}</span> 124 <Show when={props.lineNumberOld}> 125 <a 126 class="hover:text-gray-700 hover:underline hover:dark:text-gray-200" 127 href={`#${id}`} 128 > 129 {props.lineNumberOld} 130 </a> 131 </Show> 132 </span> 133 <span class="float-right mr-1 w-1/2 justify-end"> 134 {props.fillerNew} 135 <Show when={props.lineNumberNew}> 136 <a 137 class="hover:text-gray-700 hover:underline hover:dark:text-gray-200" 138 href={`#${id}`} 139 > 140 {props.lineNumberNew} 141 </a> 142 </Show> 143 </span> 144 </div> 145 <Switch> 146 <Match when={props.line.Op === 0}> 147 <div class="w-full text-gray-500 dark:text-gray-500"> 148 <div class="select-none">{"   "}</div> 149 {props.line.Line} 150 </div> 151 </Match> 152 <Match when={props.line.Op === 2}> 153 <div class="w-full bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400"> 154 <div class="select-none">{" + "}</div> 155 {props.line.Line} 156 </div> 157 </Match> 158 <Match when={props.line.Op === 1}> 159 <div class="w-full bg-red-100 text-red-700 dark:bg-red-800/30 dark:text-red-400"> 160 <div class="select-none">{" - "}</div> 161 {props.line.Line} 162 </div> 163 </Match> 164 </Switch> 165 </div> 166 ); 167} 168 169function DiffView(props: { commit: Commit }) { 170 return ( 171 <For each={props.commit.diff.diff}> 172 {(diff) => { 173 const [show, setShow] = createSignal(true); 174 const [addedLines, removedLines] = diff.text_fragments.reduce( 175 (acc, v) => [acc[0] + v.NewLines, acc[1] + v.LinesDeleted], 176 [0, 0], 177 ); 178 179 const lastFrag = diff.text_fragments[diff.text_fragments.length - 1]; 180 const numberSize = Math.max( 181 2, 182 ( 183 Math.max(lastFrag.NewPosition, lastFrag.OldPosition) + 184 lastFrag.Lines.length 185 ).toString().length, 186 ); 187 188 return ( 189 <div 190 id={`file-${diff.name.new}`} 191 class="not-last:mb-1 flex w-full flex-col rounded border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800" 192 > 193 <div 194 class={`sticky top-0 z-10 flex cursor-default select-none flex-row items-center gap-2 bg-white p-2 hover:bg-gray-100 dark:bg-gray-800 hover:dark:bg-gray-700 ${show() ? "rounded-t border-gray-200 border-b dark:border-gray-700" : "rounded"}`} 195 onclick={() => setShow(!show())} 196 > 197 <div 198 class={`iconify ${show() ? "gravity-ui--chevron-down" : "gravity-ui--chevron-right"}`} 199 /> 200 <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 201 <Show when={addedLines > 0}> 202 <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${addedLines}`}</div> 203 </Show> 204 <Show when={removedLines > 0}> 205 <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${removedLines}`}</div> 206 </Show> 207 </div> 208 <Show 209 when={diff.name.old !== "" && diff.name.new !== diff.name.old} 210 > 211 <div class="select-text">{diff.name.old}</div> 212 <div class="iconify gravity-ui--arrow-right" /> 213 </Show> 214 <div class="select-text">{diff.name.new}</div> 215 </div> 216 <div 217 class={`select-text overflow-x-auto rounded-b bg-white dark:bg-gray-800 ${show() ? "" : "hidden"}`} 218 > 219 <div class="min-w-max"> 220 <For each={diff.text_fragments}> 221 {(frag, i) => ( 222 <Fragment 223 file={diff.name.new} 224 data={frag} 225 index={i()} 226 numberSize={numberSize} 227 /> 228 )} 229 </For> 230 </div> 231 </div> 232 </div> 233 ); 234 }} 235 </For> 236 ); 237} 238 239function CommitHeader(props: { 240 user: string; 241 repo: string; 242 message: { title: string; content: string }; 243 commit: Commit; 244}) { 245 return ( 246 <div> 247 <Header user={props.user} repo={props.repo} /> 248 <div class="mx-1 flex flex-col gap-2 rounded bg-white p-4 dark:bg-gray-800"> 249 <div>{props.message.title}</div> 250 <Show when={props.message.content}> 251 <div class="text-xs">{props.message.content}</div> 252 </Show> 253 <div class="text-gray-500 text-xs dark:text-gray-300"> 254 <span>{`${new Date(props.commit.diff.commit.author.When).toLocaleDateString(undefined, { dateStyle: "long" })} at ${new Date(props.commit.diff.commit.author.When).toLocaleTimeString()}`}</span> 255 <span class="select-none px-1 before:content-['\00B7']" /> 256 <span>{`${props.commit.diff.commit.author.Name} <${props.commit.diff.commit.author.Email}>`}</span> 257 <span class="select-none px-1 before:content-['\00B7']" /> 258 <a 259 class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 260 href={`/${props.user}/${props.repo}/commit/${props.commit.ref}`} 261 > 262 {props.commit.ref.slice(0, 8)} 263 </a> 264 <Show when={props.commit.diff.commit.parent}> 265 <div class="iconify gravity-ui--arrow-left mx-1 text-[0.6rem]" /> 266 <a 267 class="hover:text-gray-600 hover:underline hover:dark:text-gray-200" 268 href={`/${props.user}/${props.repo}/commit/${props.commit.diff.commit.parent}`} 269 > 270 {props.commit.diff.commit.parent.slice(0, 8)} 271 </a> 272 </Show> 273 </div> 274 </div> 275 </div> 276 ); 277} 278 279export default function RepoCommit() { 280 const params = useParams(); 281 const did = useDid(); 282 283 const [commit] = createResource( 284 () => { 285 const d = did(); 286 if (!d) return; 287 return [d, params.repo, params.ref]; 288 }, 289 async ([did, repo, ref]) => { 290 const res = await getRepoCommit(did as DID, repo, ref); 291 if (!res.ok) return; 292 return res.data as Commit; 293 }, 294 ); 295 296 const [sidebar] = createResource(commit, async (commit) => { 297 if (!commit.diff.diff) 298 return { name: "", fullPath: "", type: "directory" } as TreeNode; 299 return buildTree(commit.diff.diff.map((v) => v.name.new)); 300 }); 301 302 const allData = createMemo(() => { 303 const s = sidebar(); 304 const c = commit(); 305 if (!(s && c)) return; 306 307 return [s, c] as const; 308 }); 309 310 const headerData = createMemo(() => { 311 const c = commit(); 312 if (!c) return; 313 314 const titleEnd = c.diff.commit.message.indexOf("\n"); 315 const message = { 316 title: c.diff.commit.message.slice(0, titleEnd), 317 content: c.diff.commit.message.slice(titleEnd + 1), 318 }; 319 320 return [c, message] as const; 321 }); 322 323 onMount(() => { 324 if (window.location.hash) { 325 const element = document.getElementById(window.location.hash.slice(1)); 326 if (element) 327 element.scrollIntoView({ behavior: "instant", block: "start" }); 328 } 329 }); 330 331 return ( 332 <div class="mx-auto max-w-10xl"> 333 <Suspense> 334 <Show when={headerData()} keyed> 335 {([commit, message]) => ( 336 <CommitHeader 337 user={params.user} 338 repo={params.repo} 339 message={message} 340 commit={commit} 341 /> 342 )} 343 </Show> 344 <Show when={allData()} keyed> 345 {([sidebar, commit]) => ( 346 <> 347 <div class="flex flex-row gap-1"> 348 <div class="sticky top-0 flex max-h-screen min-w-50 overflow-auto p-1 pr-0"> 349 <Show when={sidebar.children}> 350 <div class="flex min-h-max w-full grow cursor-default flex-col rounded border border-gray-200 bg-white p-1 dark:border-gray-700 dark:bg-gray-800"> 351 <div class="flex flex-row items-center justify-between gap-1 p-1"> 352 <div class="font-bold">CHANGED FILES</div> 353 <div class="flex h-6 select-text flex-row items-center overflow-hidden rounded font-mono text-xs *:h-full *:content-center *:px-1"> 354 <Show when={commit.diff.stat.insertions > 0}> 355 <div class="bg-green-100 text-green-700 dark:bg-green-800/30 dark:text-green-400">{`+${commit.diff.stat.insertions}`}</div> 356 </Show> 357 <Show when={commit.diff.stat.deletions > 0}> 358 <div class="bg-red-100 text-red-700 dark:bg-red-700/30 dark:text-red-400">{`-${commit.diff.stat.deletions}`}</div> 359 </Show> 360 </div> 361 </div> 362 <RenderTree tree={sidebar} skip={true} /> 363 </div> 364 </Show> 365 </div> 366 <div class="min-w-0 flex-1 flex-col gap-1 p-1 pl-0"> 367 <DiffView commit={commit} /> 368 </div> 369 </div> 370 </> 371 )} 372 </Show> 373 </Suspense> 374 </div> 375 ); 376}