extremely wip tangled spa
1import type { 2 ShTangledRepoLanguages, 3 ShTangledRepoTree, 4} from "@atcute/tangled"; 5import { type Params, useParams } from "@solidjs/router"; 6import { 7 createMemo, 8 createResource, 9 createSignal, 10 For, 11 Match, 12 Show, 13 Switch, 14} from "solid-js"; 15import { SolidMarkdown } from "solid-markdown"; 16import { IconWithText } from "../../elements/icon_with_text"; 17import { languageColors } from "../../util/get_language"; 18import type { RepoLog } from "../../util/types"; 19import { useDid } from "./context"; 20import { Header } from "./main"; 21import { 22 getRepoBranches, 23 getRepoDefaultBranch, 24 getRepoLanguages, 25 getRepoLog, 26 getRepoTree, 27} from "./main.data"; 28import "../../styles/markdown.css"; 29import { figureOutDid } from "../../util/handle"; 30 31export async function preloadRepoTree({ params }: { params: Params }) { 32 const did = await figureOutDid(params.user); 33 if (!did) return; 34 getRepoTree(did, params.repo, params.ref, params.path); 35} 36 37export default function RepoTree() { 38 const params = useParams(); 39 const did = useDid(); 40 41 const [defaultBranch] = createResource(did, async (did) => { 42 const res = await getRepoDefaultBranch(did, params.repo); 43 if (!res.ok) return; 44 return res.data.name; 45 }); 46 47 const [tree] = createResource( 48 () => { 49 const d = did(); 50 return d && ([d, params.repo, params.ref, params.path] as const); 51 }, 52 async ([d, repo, ref, path]) => { 53 const res = await getRepoTree(d, repo, ref, path); 54 if (!res.ok) return; 55 return res.data; 56 }, 57 ); 58 59 const [languages] = createResource(did, async (did) => { 60 const res = await getRepoLanguages(did, params.repo, params.ref); 61 if (!res.ok) return; 62 return res.data.languages.sort((a, b) => b.percentage - a.percentage); 63 }); 64 65 const [logs] = createResource( 66 () => { 67 const d = did(); 68 return d && ([d, params.repo, params.ref, params.path] as const); 69 }, 70 async ([d, repo, ref, path]) => { 71 const res = await getRepoLog(d, repo, ref, path); 72 if (!res.ok) return; 73 return res.data as RepoLog; 74 }, 75 ); 76 77 const [branchesAndTags] = createResource( 78 () => { 79 const d = did(); 80 return d && ([d, params.repo, params.ref] as const); 81 }, 82 async ([d, repo, ref]) => { 83 const resBranches = await getRepoBranches(d, repo, ref); 84 if (!resBranches.ok) return; 85 return resBranches.data as RepoLog; 86 }, 87 ); 88 89 const [readme] = createResource(tree, async (tree) => { 90 if (!tree.readme) return; 91 return { 92 contents: tree.readme.contents, 93 type: tree.readme.filename.toLowerCase().endsWith(".md") 94 ? "markdown" 95 : "plaintext", 96 } as const; 97 }); 98 99 const [filesInOrder] = createResource(tree, (tree) => { 100 if (!tree.files) return; 101 return tree.files.sort((a, b) => { 102 if (!a.is_file === b.is_file) return !a.is_file ? -1 : 1; 103 104 const aDot = a.name.startsWith("."); 105 const bDot = b.name.startsWith("."); 106 if (aDot !== bDot) return aDot ? -1 : 1; 107 108 return a.name.localeCompare(b.name, undefined, { numeric: true }); 109 }); 110 }); 111 112 const repoData = createMemo(() => { 113 const db = defaultBranch(); 114 const l = languages(); 115 if (!(db && l)) return; 116 return [db, l] as const; 117 }); 118 119 const pathData = createMemo(() => { 120 const t = tree(); 121 const f = filesInOrder(); 122 const r = readme(); 123 const l = logs(); 124 if (!(t && f && r && l)) return; 125 return [t, f, r, l] as const; 126 }); 127 128 return ( 129 <div class="mx-auto max-w-5xl"> 130 <Show when={repoData()} keyed> 131 {([defaultBranch, languages]) => ( 132 <Show when={pathData()} keyed> 133 {([tree, files, readme, logs]) => ( 134 <div> 135 <Header user={params.user} repo={params.repo} /> 136 <div class="mb-4 flex flex-col rounded bg-white dark:bg-gray-800"> 137 <LanguageLine languages={languages} /> 138 <div class="flex flex-row"> 139 <div class="mr-1 flex w-1/2 flex-col border-gray-300 border-r p-2 pt-1 dark:border-gray-700"> 140 <FileDirectory 141 user={params.user} 142 repo={params.repo} 143 files={files} 144 tree={tree} 145 defaultBranch={defaultBranch} 146 /> 147 </div> 148 <div class="ml-1 flex w-1/2 flex-col p-2 pt-1"> 149 <LogData 150 user={params.user} 151 repo={params.repo} 152 defaultBranch={defaultBranch} 153 files={files} 154 logs={logs} 155 /> 156 </div> 157 </div> 158 </div> 159 <Show when={readme.contents}> 160 <ReadmeCard 161 path={`/${params.user}/${params.repo}/blob/${tree.ref || defaultBranch}`} 162 readme={readme} 163 /> 164 </Show> 165 </div> 166 )} 167 </Show> 168 )} 169 </Show> 170 </div> 171 ); 172} 173 174function ReadmeCard(props: { 175 path: string; 176 readme: { 177 type: "markdown" | "plaintext"; 178 contents: string; 179 }; 180}) { 181 return ( 182 <div class="mb-4 rounded bg-white p-4 dark:bg-gray-800"> 183 <Switch> 184 <Match when={props.readme.type === "markdown"}> 185 <SolidMarkdown 186 class="markdown" 187 transformImageUri={(href) => `${props.path}${href}`} 188 transformLinkUri={(href) => `${props.path}${href}`} 189 renderingStrategy="memo" 190 children={props.readme.contents} 191 /> 192 </Match> 193 <Match when={props.readme.type === "plaintext"}> 194 <div class="font-mono"> {props.readme.contents}</div> 195 </Match> 196 </Switch> 197 </div> 198 ); 199} 200 201function LanguageLine(props: { languages: ShTangledRepoLanguages.Language[] }) { 202 const [languageLineState, setLanguageLineState] = createSignal(false); 203 const toggleLanguageLineState = () => 204 setLanguageLineState(!languageLineState()); 205 206 return ( 207 <div class={languageLineState() ? "h-full" : "h-4"}> 208 <div 209 class={`flex w-full flex-row overflow-hidden rounded-t duration-75 hover:h-4 ${languageLineState() ? "h-4" : "h-2"}`} 210 onclick={toggleLanguageLineState} 211 > 212 <For each={props.languages}> 213 {(language) => ( 214 <div 215 class="h-full border-gray-50 border-r duration-75 hover:brightness-90 dark:border-gray-950 dark:hover:brightness-110" 216 style={`width: ${language.percentage}%; background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`} 217 title={`${language.name} ${language.percentage}%`} 218 ></div> 219 )} 220 </For> 221 </div> 222 <div 223 class={`flex h-4 flex-row gap-3 border-gray-300 border-r border-b px-6 py-3.5 text-xs dark:border-gray-700 ${languageLineState() ? "" : "hidden"}`} 224 > 225 <For each={props.languages}> 226 {(language) => ( 227 <div class="flex flex-row items-center gap-2"> 228 <div 229 class="h-2 w-2 rounded-full" 230 style={`background-color: ${languageColors.get(language.name.toLowerCase().replaceAll(" ", ""))}`} 231 /> 232 <span> 233 <span>{language.name}</span>{" "} 234 <span class="text-gray-600 dark:text-gray-400"> 235 {language.percentage}% 236 </span> 237 </span> 238 </div> 239 )} 240 </For> 241 </div> 242 </div> 243 ); 244} 245 246function FileDirectory(props: { 247 user: string; 248 repo: string; 249 files: ShTangledRepoTree.TreeEntry[]; 250 tree: ShTangledRepoTree.$output; 251 defaultBranch: string; 252}) { 253 return ( 254 <div class="flex w-full flex-col"> 255 <Show when={props.tree.parent}> 256 <a 257 href={`/${props.user}/${props.repo}/tree/${props.tree.ref}/${props.tree.dotdot ? `/${props.tree.dotdot}` : ""}`} 258 class="flex flex-row items-center gap-1 p-1 hover:underline" 259 > 260 <div class="iconify gravity-ui--folder-open-fill" /> 261 <span>..</span> 262 </a> 263 </Show> 264 265 <For each={props.files}> 266 {(file) => ( 267 <a 268 href={`/${props.user}/${props.repo}/${file.is_file ? "blob" : "tree"}/${props.tree.ref || props.defaultBranch}/${props.tree.parent ? `${props.tree.parent}/` : ""}${file.name}`} 269 class="flex flex-row items-center gap-1 p-1 hover:underline" 270 > 271 <div 272 class={`iconify ${file.is_file ? "gravity-ui--file" : "gravity-ui--folder-fill"}`} 273 /> 274 <span>{file.name}</span> 275 </a> 276 )} 277 </For> 278 </div> 279 ); 280} 281 282function LogData(props: { 283 user: string; 284 repo: string; 285 defaultBranch: string; 286 files: ShTangledRepoTree.TreeEntry[]; 287 logs: RepoLog; 288}) { 289 return ( 290 <> 291 <a 292 class="mb-2 flex flex-row items-center gap-2 text-black hover:text-gray-600 dark:text-white hover:dark:text-gray-300" 293 href={`/${props.user}/${props.repo}/commits/${props.logs.ref || props.defaultBranch}`} 294 > 295 <IconWithText 296 icon="gravity-ui--code-commit" 297 text="commits" 298 style="font-bold" 299 /> 300 <div class="rounded bg-gray-300 px-1 text-xs dark:bg-gray-700"> 301 {props.logs.total} 302 </div> 303 </a> 304 <div class="mb-3 flex flex-col gap-4"> 305 <For 306 each={props.logs.commits.slice( 307 0, 308 Math.max(3, Math.floor(props.files.length / 2)), 309 )} 310 > 311 {(commit) => { 312 const hash = commit.Hash.map((num) => 313 num.toString(16).padStart(2, "0"), 314 ).join(""); 315 return ( 316 <div class="flex flex-col gap-1"> 317 <a 318 class="text-black hover:text-gray-600 hover:underline dark:text-white hover:dark:text-gray-300" 319 href={`/${props.user}/${props.repo}/commit/${hash}`} 320 > 321 {commit.Message.slice(0, commit.Message.indexOf("\n"))} 322 </a> 323 <span class="text-gray-500 text-xs dark:text-gray-300"> 324 <span class="rounded bg-gray-100 p-1 font-mono dark:bg-gray-900"> 325 {hash.slice(0, 8)} 326 </span> 327 <span class="select-none px-1 before:content-['\00B7']" /> 328 <span>{commit.Author.Name}</span> 329 <span class="select-none px-1 before:content-['\00B7']" /> 330 <span>{`${new Date(commit.Author.When).toLocaleDateString(undefined, { dateStyle: "short" })} at ${new Date(commit.Author.When).toLocaleTimeString()}`}</span> 331 </span> 332 </div> 333 ); 334 }} 335 </For> 336 </div> 337 </> 338 ); 339}