atmosphere explorer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

expand row on hover

Juliet 71e558c6 5aa6bdc1

+210 -165
+1 -1
index.html
··· 18 18 title="PDSls" 19 19 /> 20 20 <link rel="preconnect" href="https://fonts.bunny.net" /> 21 - <link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" /> 21 + <link href="https://fonts.bunny.net/css?family=roboto-mono:400,500" rel="stylesheet" /> 22 22 <script> 23 23 document.documentElement.classList.toggle( 24 24 "dark",
+209 -164
src/views/pds.tsx
··· 6 6 import { createWindowVirtualizer } from "@tanstack/solid-virtual"; 7 7 import { createEffect, createResource, createSignal, For, on, onCleanup, Show } from "solid-js"; 8 8 import { Button } from "../components/button"; 9 - import DidHoverCard from "../components/hover-card/did"; 10 9 import { setPDS } from "../components/navbar"; 11 10 import { canHover } from "../layout"; 11 + import { didDocCache, resolveDidDoc } from "../utils/api"; 12 12 import { localDateFromTimestamp } from "../utils/date"; 13 13 14 14 const LIMIT = 1000; ··· 18 18 expanded: boolean; 19 19 onToggle: () => void; 20 20 }) => { 21 - const expanded = () => props.expanded; 21 + const [hovered, setHovered] = createSignal(false); 22 + const expanded = () => props.expanded || hovered(); 22 23 const [collapsing, setCollapsing] = createSignal(false); 24 + let hoverTimeout: number | null = null; 23 25 24 26 createEffect( 25 27 on(expanded, (curr, prev) => { ··· 31 33 }), 32 34 ); 33 35 34 - const animating = () => expanded() || collapsing(); 36 + const [handle] = createResource( 37 + () => (expanded() ? props.repo.did : null), 38 + async (did) => { 39 + try { 40 + const doc = 41 + didDocCache[did] ?? 42 + (didDocCache[did] = await resolveDidDoc(did as `did:${string}:${string}`)); 43 + return ( 44 + doc.alsoKnownAs?.find((aka) => aka.startsWith("at://"))?.replace("at://", "") ?? null 45 + ); 46 + } catch { 47 + return null; 48 + } 49 + }, 50 + ); 51 + 52 + const handleMouseEnter = () => { 53 + if (!canHover) return; 54 + hoverTimeout = window.setTimeout(() => { 55 + setHovered(true); 56 + hoverTimeout = null; 57 + }, 300); 58 + }; 59 + 60 + const handleMouseLeave = () => { 61 + if (!canHover) return; 62 + if (hoverTimeout !== null) { 63 + clearTimeout(hoverTimeout); 64 + hoverTimeout = null; 65 + } 66 + setHovered(false); 67 + }; 68 + 69 + onCleanup(() => { 70 + if (hoverTimeout !== null) clearTimeout(hoverTimeout); 71 + }); 35 72 36 73 return ( 37 74 <div ··· 39 76 "group relative rounded-md border-[0.5px]": true, 40 77 "z-20": expanded(), 41 78 "z-10": collapsing(), 42 - "transition-[background-color,border-color,box-shadow] duration-250": animating(), 79 + "transition-[background-color,border-color,box-shadow] duration-250": 80 + expanded() || collapsing(), 43 81 "dark:hover:bg-dark-200 border-transparent hover:bg-neutral-200/50": !expanded(), 44 82 "dark:bg-dark-300 border-neutral-200 bg-neutral-50 shadow-sm dark:border-neutral-700 dark:shadow-dark-700": 45 83 expanded(), 46 84 }} 85 + onMouseEnter={handleMouseEnter} 86 + onMouseLeave={handleMouseLeave} 47 87 > 48 88 <div class="flex min-w-0 flex-1 items-center"> 49 89 <button 50 90 type="button" 51 - onclick={() => props.onToggle()} 91 + onclick={() => { 92 + if (!canHover) props.onToggle(); 93 + }} 52 94 class="flex min-w-0 flex-1 items-center gap-2 p-1.5" 53 95 > 54 96 <span ··· 59 101 > 60 102 <span class="iconify lucide--chevron-right"></span> 61 103 </span> 62 - <div class="flex min-w-0 flex-1 items-center gap-x-2 text-sm"> 104 + <div class="flex min-h-5 min-w-0 flex-1 items-center gap-x-2 text-xs sm:text-sm"> 63 105 <span class="min-w-0 truncate font-mono" onclick={(e) => e.stopPropagation()}> 64 - <DidHoverCard newTab did={props.repo.did} /> 106 + <A 107 + href={`/at://${props.repo.did}`} 108 + class="text-blue-500 hover:underline dark:text-blue-400" 109 + > 110 + {props.repo.did} 111 + </A> 65 112 </span> 66 113 <Show when={!props.repo.active}> 67 114 <span class="flex shrink-0 items-center gap-1 text-red-500 dark:text-red-400"> ··· 81 128 <A 82 129 href={`/at://${props.repo.did}`} 83 130 classList={{ 84 - "flex shrink-0 items-center p-2 transition-colors duration-500": true, 131 + "flex shrink-0 items-center p-2 transition-colors duration-250": true, 85 132 "invisible group-hover:visible not-hover:text-neutral-500 not-hover:dark:text-neutral-400": 86 133 !expanded(), 87 134 }} ··· 99 146 > 100 147 <div class="overflow-hidden"> 101 148 <div class="ml-7.5 flex flex-col gap-1.5 pb-1.5 font-mono text-xs text-neutral-500 dark:text-neutral-400"> 102 - <Show when={props.repo.head}> 103 - <span class="truncate">{props.repo.head}</span> 149 + <Show when={handle.loading}> 150 + <span class="animate-pulse">resolving...</span> 151 + </Show> 152 + <Show when={!handle.loading && handle()}> 153 + <span class="font-medium text-neutral-900 dark:text-neutral-200">@{handle()}</span> 104 154 </Show> 105 155 <Show when={TID.validate(props.repo.rev)}> 106 156 <div class="flex gap-1 text-neutral-700 dark:text-neutral-300"> ··· 109 159 <span>{localDateFromTimestamp(TID.parse(props.repo.rev).timestamp / 1000)}</span> 110 160 </div> 111 161 </Show> 162 + <Show when={props.repo.head}> 163 + <span class="truncate">{props.repo.head}</span> 164 + </Show> 112 165 </div> 113 166 </div> 114 167 </div> ··· 116 169 ); 117 170 }; 118 171 172 + const InfoField = (props: { label: string; children: any }) => ( 173 + <div class="flex flex-col"> 174 + <span class="font-semibold">{props.label}</span> 175 + {props.children} 176 + </div> 177 + ); 178 + 119 179 export const PdsView = () => { 120 180 const params = useParams(); 121 181 const location = useLocation(); ··· 129 189 const rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 130 190 131 191 const getVersion = async () => { 132 - // @ts-expect-error: undocumented endpoint 133 - const res = await rpc.get("_health", {}); 134 - setVersion((res.data as any).version); 192 + try { 193 + // @ts-expect-error: undocumented endpoint 194 + const res = await rpc.get("_health", {}); 195 + setVersion((res.data as any).version); 196 + } catch (err) { 197 + console.error("Failed to fetch version:", err); 198 + } 135 199 }; 136 200 137 201 const describeServer = async () => { ··· 209 273 document.title = `${params.pds} - PDSls`; 210 274 211 275 return ( 212 - <> 213 - <Show when={repos() || response()}> 214 - <div class="flex w-full flex-col px-2"> 215 - <div class="mb-3 flex gap-4 text-sm sm:text-base"> 216 - <Tab tab="repos" label="Repositories" /> 217 - <Tab tab="info" label="Info" /> 218 - <Tab tab="firehose" label="Firehose" /> 276 + <Show when={repos() || response()}> 277 + <div class="flex w-full flex-col px-2"> 278 + <div class="mb-3 flex gap-4 text-sm sm:text-base"> 279 + <Tab tab="repos" label="Repositories" /> 280 + <Tab tab="info" label="Info" /> 281 + <Tab tab="firehose" label="Firehose" /> 282 + </div> 283 + <Show when={!location.hash || location.hash === "#repos"}> 284 + <div 285 + class="-mx-2 mb-9" 286 + ref={containerRef} 287 + style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }} 288 + > 289 + <For each={virtualizer.getVirtualItems()}> 290 + {(virtualItem) => { 291 + const isExpanded = () => expandedIndex() === virtualItem.index; 292 + return ( 293 + <div 294 + data-index={virtualItem.index} 295 + ref={virtualizer.measureElement} 296 + style={{ 297 + position: "absolute", 298 + top: `${virtualItem.start - virtualizer.options.scrollMargin}px`, 299 + left: 0, 300 + width: "100%", 301 + overflow: "visible", 302 + }} 303 + > 304 + <RepoCard 305 + repo={repos()![virtualItem.index]} 306 + expanded={isExpanded()} 307 + onToggle={() => setExpandedIndex(isExpanded() ? null : virtualItem.index)} 308 + /> 309 + </div> 310 + ); 311 + }} 312 + </For> 219 313 </div> 220 - <Show when={!location.hash || location.hash === "#repos"}> 221 - <div 222 - class="-mx-2 mb-9" 223 - ref={containerRef} 224 - style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }} 225 - > 226 - <For each={virtualizer.getVirtualItems()}> 227 - {(virtualItem) => { 228 - const isExpanded = () => expandedIndex() === virtualItem.index; 229 - return ( 230 - <div 231 - data-index={virtualItem.index} 232 - ref={virtualizer.measureElement} 233 - style={{ 234 - position: "absolute", 235 - top: `${virtualItem.start - virtualizer.options.scrollMargin}px`, 236 - left: 0, 237 - width: "100%", 238 - overflow: "visible", 314 + </Show> 315 + <div class="flex flex-col gap-3"> 316 + <Show when={location.hash === "#info"}> 317 + <Show when={version()}> 318 + {(version) => ( 319 + <InfoField label="Version"> 320 + <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 321 + </InfoField> 322 + )} 323 + </Show> 324 + <Show when={serverInfos()}> 325 + {(server) => ( 326 + <> 327 + <InfoField label="DID"> 328 + <span class="text-sm text-neutral-700 dark:text-neutral-300"> 329 + {server().did} 330 + </span> 331 + </InfoField> 332 + <div class="flex items-center gap-1"> 333 + <span class="font-semibold">Invite Code Required</span> 334 + <span 335 + classList={{ 336 + "iconify lucide--check text-green-500 dark:text-green-400": 337 + server().inviteCodeRequired === true, 338 + "iconify lucide--x text-red-500 dark:text-red-400": 339 + !server().inviteCodeRequired, 239 340 }} 240 - > 241 - <RepoCard 242 - repo={repos()![virtualItem.index]} 243 - expanded={isExpanded()} 244 - onToggle={() => { 245 - setExpandedIndex(isExpanded() ? null : virtualItem.index); 246 - }} 247 - /> 248 - </div> 249 - ); 250 - }} 251 - </For> 252 - </div> 253 - </Show> 254 - <div class="flex flex-col gap-3"> 255 - <Show when={location.hash === "#info"}> 256 - <Show when={version()}> 257 - {(version) => ( 258 - <div class="flex flex-col"> 259 - <span class="font-semibold">Version</span> 260 - <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 341 + ></span> 261 342 </div> 262 - )} 263 - </Show> 264 - <Show when={serverInfos()}> 265 - {(server) => ( 266 - <> 267 - <div class="flex flex-col"> 268 - <span class="font-semibold">DID</span> 269 - <span class="text-sm text-neutral-700 dark:text-neutral-300"> 270 - {server().did} 271 - </span> 272 - </div> 343 + <Show when={server().phoneVerificationRequired}> 273 344 <div class="flex items-center gap-1"> 274 - <span class="font-semibold">Invite Code Required</span> 275 - <span 276 - classList={{ 277 - "iconify lucide--check text-green-500 dark:text-green-400": 278 - server().inviteCodeRequired === true, 279 - "iconify lucide--x text-red-500 dark:text-red-400": 280 - !server().inviteCodeRequired, 281 - }} 282 - ></span> 345 + <span class="font-semibold">Captcha Verification Required</span> 346 + <span class="iconify lucide--check text-green-500 dark:text-green-400"></span> 283 347 </div> 284 - <Show when={server().phoneVerificationRequired}> 285 - <div class="flex items-center gap-1"> 286 - <span class="font-semibold">Captcha Verification Required</span> 287 - <span class="iconify lucide--check text-green-500 dark:text-green-400"></span> 288 - </div> 289 - </Show> 290 - <Show when={server().availableUserDomains.length}> 291 - <div class="flex flex-col"> 292 - <span class="font-semibold">Available User Domains</span> 293 - <For each={server().availableUserDomains}> 294 - {(domain) => ( 295 - <span class="text-sm wrap-anywhere text-neutral-700 dark:text-neutral-300"> 296 - {domain} 297 - </span> 298 - )} 299 - </For> 300 - </div> 301 - </Show> 302 - <Show when={server().links?.privacyPolicy}> 303 - <div class="flex flex-col"> 304 - <span class="font-semibold">Privacy Policy</span> 305 - <a 306 - href={server().links?.privacyPolicy} 307 - class="text-sm text-neutral-700 hover:underline dark:text-neutral-300" 308 - target="_blank" 309 - rel="noopener" 310 - > 311 - {server().links?.privacyPolicy} 312 - </a> 313 - </div> 314 - </Show> 315 - <Show when={server().links?.termsOfService}> 316 - <div class="flex flex-col"> 317 - <span class="font-semibold">Terms of Service</span> 348 + </Show> 349 + <Show when={server().availableUserDomains.length}> 350 + <InfoField label="Available User Domains"> 351 + <For each={server().availableUserDomains}> 352 + {(domain) => ( 353 + <span class="text-sm wrap-anywhere text-neutral-700 dark:text-neutral-300"> 354 + {domain} 355 + </span> 356 + )} 357 + </For> 358 + </InfoField> 359 + </Show> 360 + <For 361 + each={[ 362 + { label: "Privacy Policy", url: server().links?.privacyPolicy }, 363 + { label: "Terms of Service", url: server().links?.termsOfService }, 364 + { 365 + label: "Contact", 366 + url: 367 + server().contact?.email ? `mailto:${server().contact?.email}` : undefined, 368 + display: server().contact?.email, 369 + }, 370 + ].filter((l) => l.url)} 371 + > 372 + {(link) => ( 373 + <InfoField label={link.label}> 318 374 <a 319 - href={server().links?.termsOfService} 375 + href={link.url} 320 376 class="text-sm text-neutral-700 hover:underline dark:text-neutral-300" 321 377 target="_blank" 322 378 rel="noopener" 323 379 > 324 - {server().links?.termsOfService} 380 + {link.display ?? link.url} 325 381 </a> 326 - </div> 327 - </Show> 328 - <Show when={server().contact?.email}> 329 - <div class="flex flex-col"> 330 - <span class="font-semibold">Contact</span> 331 - <a 332 - href={`mailto:${server().contact?.email}`} 333 - class="text-sm text-neutral-700 hover:underline dark:text-neutral-300" 334 - > 335 - {server().contact?.email} 336 - </a> 337 - </div> 338 - </Show> 339 - </> 340 - )} 382 + </InfoField> 383 + )} 384 + </For> 385 + </> 386 + )} 387 + </Show> 388 + </Show> 389 + </div> 390 + </div> 391 + <Show when={!location.hash || location.hash === "#repos"}> 392 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center border-t border-neutral-200 bg-neutral-100 pt-3 pb-6 dark:border-neutral-700"> 393 + <div class="flex items-center gap-3"> 394 + <p> 395 + {repos()?.length} loaded 396 + <Show when={repos()?.some((r) => !r.active)}> 397 + {" · "} 398 + <span class="text-neutral-500 dark:text-neutral-400"> 399 + {repos()?.filter((r) => !r.active).length} inactive 400 + </span> 341 401 </Show> 402 + </p> 403 + <Show when={cursor()}> 404 + <Button 405 + onClick={() => { 406 + setExpandedIndex(null); 407 + refetch(); 408 + }} 409 + disabled={response.loading} 410 + classList={{ "w-20 h-7.5 justify-center": true }} 411 + > 412 + <Show 413 + when={!response.loading} 414 + fallback={<span class="iconify lucide--loader-circle animate-spin text-base" />} 415 + > 416 + Load more 417 + </Show> 418 + </Button> 342 419 </Show> 343 420 </div> 344 421 </div> 345 - <Show when={!location.hash || location.hash === "#repos"}> 346 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center border-t border-neutral-200 bg-neutral-100 pt-3 pb-6 dark:border-neutral-700"> 347 - <div class="flex items-center gap-3"> 348 - <p> 349 - {repos()?.length} loaded 350 - <Show when={repos()?.some((r) => !r.active)}> 351 - {" · "} 352 - <span class="text-neutral-500 dark:text-neutral-400"> 353 - {repos()?.filter((r) => !r.active).length} inactive 354 - </span> 355 - </Show> 356 - </p> 357 - <Show when={cursor()}> 358 - <Button 359 - onClick={() => { 360 - setExpandedIndex(null); 361 - refetch(); 362 - }} 363 - disabled={response.loading} 364 - classList={{ "w-20 h-7.5 justify-center": true }} 365 - > 366 - <Show 367 - when={!response.loading} 368 - fallback={<span class="iconify lucide--loader-circle animate-spin text-base" />} 369 - > 370 - Load more 371 - </Show> 372 - </Button> 373 - </Show> 374 - </div> 375 - </div> 376 - </Show> 377 422 </Show> 378 - </> 423 + </Show> 379 424 ); 380 425 };