my website at ewancroft.uk

refactor+chore(tangled): consolidate and update

ewancroft.uk 7d861a87 20a3f705

verified
Changed files
+171 -184
src
lib
+1 -1
README.md
··· 507 507 - [teal.fm](https://teal.fm/) 508 508 - [kibun.social](https://kibun.social/) 509 509 - [MusicBrainz](https://musicbrainz.org/) 510 - - [Tangled](https://tangled.sh/) 510 + - [Tangled](https://tangled.org/) 511 511 - [Linkat](https://linkat.blue/) 512 512 513 513 ## 💡 Tips & Troubleshooting
-73
src/lib/components/layout/main/TangledRepos.svelte
··· 1 - <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import { Card } from '$lib/components/ui'; 4 - import { TangledRepoCard } from '$lib/components/layout/main/card'; 5 - import { fetchTangledRepos, type TangledReposData, fetchProfile } from '$lib/services/atproto'; 6 - 7 - let repos: TangledReposData | null = null; 8 - let handle: string | null = null; 9 - let loading = true; 10 - let error: string | null = null; 11 - 12 - onMount(async () => { 13 - try { 14 - const [reposData, profile] = await Promise.all([fetchTangledRepos(), fetchProfile()]); 15 - repos = reposData; 16 - handle = profile.handle; 17 - } catch (err) { 18 - error = err instanceof Error ? err.message : 'Failed to load Tangled repositories'; 19 - } finally { 20 - loading = false; 21 - } 22 - }); 23 - </script> 24 - 25 - <div class="mx-auto w-full max-w-2xl"> 26 - {#if loading} 27 - <Card loading={true} variant="elevated" padding="md"> 28 - {#snippet skeleton()} 29 - <div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div> 30 - <div class="space-y-3"> 31 - {#each Array(3) as _} 32 - <div class="h-24 rounded-lg bg-canvas-300 dark:bg-canvas-700"></div> 33 - {/each} 34 - </div> 35 - {/snippet} 36 - </Card> 37 - {:else if error} 38 - <Card error={true} errorMessage={error} /> 39 - {:else if repos && repos.repos.length > 0} 40 - {@const safeRepos = repos} 41 - <Card variant="elevated" padding="md"> 42 - {#snippet children()} 43 - <h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Tangled Repositories</h2> 44 - <div class="space-y-3"> 45 - {#each safeRepos.repos as repo} 46 - <TangledRepoCard {repo} {handle} /> 47 - {/each} 48 - </div> 49 - {/snippet} 50 - </Card> 51 - {:else} 52 - <Card variant="flat" padding="lg"> 53 - {#snippet children()} 54 - <div class="text-center"> 55 - <p class="text-ink-700 dark:text-ink-300"> 56 - No Tangled repositories found. Create a <code 57 - class="rounded bg-canvas-200 px-1 dark:bg-canvas-800">sh.tangled.repo</code 58 - > record to display your repositories here. 59 - </p> 60 - <p class="mt-2 text-sm text-ink-600 dark:text-ink-400"> 61 - Learn more about Tangled at 62 - <a 63 - href="https://tangled.sh/" 64 - class="text-primary-600 hover:underline dark:text-primary-400" 65 - target="_blank" 66 - rel="noopener noreferrer">https://tangled.org/</a 67 - > 68 - </p> 69 - </div> 70 - {/snippet} 71 - </Card> 72 - {/if} 73 - </div>
+94 -33
src/lib/components/layout/main/card/TangledRepoCard.svelte
··· 1 1 <script lang="ts"> 2 + import { onMount } from 'svelte'; 2 3 import { ExternalLink, GitBranch, Server, User } from '@lucide/svelte'; 3 - import { InternalCard } from '$lib/components/ui'; 4 - import type { TangledRepo } from '$lib/services/atproto'; 4 + import { Card, InternalCard } from '$lib/components/ui'; 5 + import { fetchTangledRepos, type TangledReposData, fetchProfile } from '$lib/services/atproto'; 5 6 import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 6 7 7 - interface Props { 8 - repo: TangledRepo; 9 - handle: string | null; 10 - } 8 + let repos: TangledReposData | null = $state(null); 9 + let handle: string | null = $state(null); 10 + let loading = $state(true); 11 + let error: string | null = $state(null); 11 12 12 - let { repo, handle }: Props = $props(); 13 + onMount(async () => { 14 + try { 15 + const [reposData, profile] = await Promise.all([fetchTangledRepos(), fetchProfile()]); 16 + repos = reposData; 17 + handle = profile.handle; 18 + } catch (err) { 19 + error = err instanceof Error ? err.message : 'Failed to load Tangled repositories'; 20 + } finally { 21 + loading = false; 22 + } 23 + }); 13 24 14 25 // Build the tangled.org URL: tangled.org/[handle or did]/[repo] 15 26 // Prefer handle if available, otherwise use DID 16 - const identifier = $derived(handle || PUBLIC_ATPROTO_DID); 17 - const repoUrl = $derived(`https://tangled.org/${identifier}/${repo.name}`); 27 + function buildRepoUrl(repoName: string): string { 28 + const identifier = handle || PUBLIC_ATPROTO_DID; 29 + return `https://tangled.org/${identifier}/${repoName}`; 30 + } 18 31 19 32 // Extract knot server name from DID or URL 20 33 function getKnotServerName(knot: string): string { ··· 30 43 } 31 44 </script> 32 45 33 - <InternalCard href={repoUrl}> 34 - {#snippet children()} 35 - <GitBranch class="h-5 w-5 shrink-0 text-primary-600 dark:text-primary-400" aria-hidden="true" /> 36 - <div class="min-w-0 flex-1 space-y-2"> 37 - <h3 38 - class="overflow-wrap-anywhere font-semibold wrap-break-word text-ink-900 dark:text-ink-50" 39 - > 40 - {repo.name} 41 - </h3> 42 - <div class="flex flex-wrap items-center gap-3 text-xs text-ink-700 dark:text-ink-200"> 43 - <div class="flex min-w-0 items-center gap-1"> 44 - <Server class="h-3 w-3 shrink-0" aria-hidden="true" /> 45 - <span class="truncate">{getKnotServerName(repo.knot)}</span> 46 + <div class="mx-auto w-full max-w-2xl"> 47 + {#if loading} 48 + <Card loading={true} variant="elevated" padding="md"> 49 + {#snippet skeleton()} 50 + <div class="mb-4 h-6 w-32 rounded bg-canvas-300 dark:bg-canvas-700"></div> 51 + <div class="space-y-3"> 52 + {#each Array(3) as _} 53 + <div class="h-24 rounded-lg bg-canvas-300 dark:bg-canvas-700"></div> 54 + {/each} 55 + </div> 56 + {/snippet} 57 + </Card> 58 + {:else if error} 59 + <Card error={true} errorMessage={error} /> 60 + {:else if repos && repos.repos.length > 0} 61 + {@const safeRepos = repos} 62 + <Card variant="elevated" padding="md"> 63 + {#snippet children()} 64 + <h2 class="mb-4 text-2xl font-bold text-ink-900 dark:text-ink-50">Tangled Repositories</h2> 65 + <div class="space-y-3"> 66 + {#each safeRepos.repos as repo} 67 + <InternalCard href={buildRepoUrl(repo.name)}> 68 + {#snippet children()} 69 + <GitBranch class="h-5 w-5 shrink-0 text-primary-600 dark:text-primary-400" aria-hidden="true" /> 70 + <div class="min-w-0 flex-1 space-y-2"> 71 + <h3 72 + class="overflow-wrap-anywhere font-semibold wrap-break-word text-ink-900 dark:text-ink-50" 73 + > 74 + {repo.name} 75 + </h3> 76 + <div class="flex flex-wrap items-center gap-3 text-xs text-ink-700 dark:text-ink-200"> 77 + <div class="flex min-w-0 items-center gap-1"> 78 + <Server class="h-3 w-3 shrink-0" aria-hidden="true" /> 79 + <span class="truncate">{getKnotServerName(repo.knot)}</span> 80 + </div> 81 + <div class="flex min-w-0 items-center gap-1"> 82 + <User class="h-3 w-3 shrink-0" aria-hidden="true" /> 83 + <span class="truncate">{handle || PUBLIC_ATPROTO_DID}</span> 84 + </div> 85 + </div> 86 + </div> 87 + <ExternalLink 88 + class="h-4 w-4 shrink-0 text-ink-700 transition-colors dark:text-ink-200" 89 + aria-hidden="true" 90 + /> 91 + {/snippet} 92 + </InternalCard> 93 + {/each} 46 94 </div> 47 - <div class="flex min-w-0 items-center gap-1"> 48 - <User class="h-3 w-3 shrink-0" aria-hidden="true" /> 49 - <span class="truncate">{handle || PUBLIC_ATPROTO_DID}</span> 95 + {/snippet} 96 + </Card> 97 + {:else} 98 + <Card variant="flat" padding="lg"> 99 + {#snippet children()} 100 + <div class="text-center"> 101 + <p class="text-ink-700 dark:text-ink-300"> 102 + No Tangled repositories found. Create a <code 103 + class="rounded bg-canvas-200 px-1 dark:bg-canvas-800">sh.tangled.repo</code 104 + > record to display your repositories here. 105 + </p> 106 + <p class="mt-2 text-sm text-ink-600 dark:text-ink-400"> 107 + Learn more about Tangled at 108 + <a 109 + href="https://tangled.sh/" 110 + class="text-primary-600 hover:underline dark:text-primary-400" 111 + target="_blank" 112 + rel="noopener noreferrer">https://tangled.org/</a 113 + > 114 + </p> 50 115 </div> 51 - </div> 52 - </div> 53 - <ExternalLink 54 - class="h-4 w-4 shrink-0 text-ink-700 transition-colors dark:text-ink-200" 55 - aria-hidden="true" 56 - /> 57 - {/snippet} 58 - </InternalCard> 116 + {/snippet} 117 + </Card> 118 + {/if} 119 + </div>
+1 -1
src/lib/components/layout/main/index.ts
··· 1 1 export { default as DynamicLinks } from './DynamicLinks.svelte'; 2 2 export { default as ScrollToTop } from './ScrollToTop.svelte'; 3 - export { default as TangledRepos } from './TangledRepos.svelte'; 3 + export { default as TangledRepos } from './card/TangledRepoCard.svelte';
+55 -1
src/lib/services/atproto/fetch.ts
··· 7 7 SiteInfoData, 8 8 LinkData, 9 9 MusicStatusData, 10 - KibunStatusData 10 + KibunStatusData, 11 + TangledRepo, 12 + TangledReposData 11 13 } from './types'; 12 14 import { buildPdsBlobUrl } from './media'; 13 15 import { findArtwork } from './musicbrainz'; ··· 398 400 return null; 399 401 } 400 402 } 403 + 404 + /** 405 + * Fetches Tangled repositories from AT Protocol 406 + */ 407 + export async function fetchTangledRepos(fetchFn?: typeof fetch): Promise<TangledReposData | null> { 408 + const cacheKey = `tangled:${PUBLIC_ATPROTO_DID}`; 409 + const cached = cache.get<TangledReposData>(cacheKey); 410 + if (cached) return cached; 411 + 412 + try { 413 + // Custom collection, prefer PDS first 414 + const records = await withFallback( 415 + PUBLIC_ATPROTO_DID, 416 + async (agent) => { 417 + const response = await agent.com.atproto.repo.listRecords({ 418 + repo: PUBLIC_ATPROTO_DID, 419 + collection: 'sh.tangled.repo', 420 + limit: 100 421 + }); 422 + return response.data.records; 423 + }, 424 + true, 425 + fetchFn 426 + ); // usePDSFirst = true 427 + 428 + if (records.length === 0) return null; 429 + 430 + const repos: TangledRepo[] = records.map((record) => { 431 + const value = record.value as any; 432 + return { 433 + uri: record.uri, 434 + name: value.name, 435 + description: value.description, 436 + knot: value.knot, 437 + createdAt: value.createdAt, 438 + labels: value.labels, 439 + source: value.source, 440 + spindle: value.spindle 441 + }; 442 + }); 443 + 444 + // Sort by creation date, newest first 445 + repos.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 446 + 447 + const data: TangledReposData = { repos }; 448 + cache.set(cacheKey, data); 449 + return data; 450 + } catch (error) { 451 + console.error('Failed to fetch Tangled repos from all sources:', error); 452 + return null; 453 + } 454 + }
+5 -6
src/lib/services/atproto/index.ts
··· 30 30 CacheEntry, 31 31 MusicStatusData, 32 32 MusicArtist, 33 - KibunStatusData 33 + KibunStatusData, 34 + TangledRepo, 35 + TangledReposData 34 36 } from './types'; 35 - 36 - export type { TangledRepo, TangledReposData } from './tangled'; 37 37 38 38 // Export fetch functions 39 39 export { ··· 41 41 fetchSiteInfo, 42 42 fetchLinks, 43 43 fetchMusicStatus, 44 - fetchKibunStatus 44 + fetchKibunStatus, 45 + fetchTangledRepos 45 46 } from './fetch'; 46 - 47 - export { fetchTangledRepos } from './tangled'; 48 47 49 48 export { 50 49 fetchBlogPosts,
-69
src/lib/services/atproto/tangled.ts
··· 1 - import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 2 - import { cache } from './cache'; 3 - import { withFallback } from './agents'; 4 - 5 - export interface TangledRepo { 6 - uri: string; 7 - name: string; 8 - description?: string; 9 - knot: string; 10 - createdAt: string; 11 - labels?: string[]; 12 - source?: string; 13 - spindle?: string; 14 - } 15 - 16 - export interface TangledReposData { 17 - repos: TangledRepo[]; 18 - } 19 - 20 - /** 21 - * Fetches Tangled repositories from AT Protocol 22 - */ 23 - export async function fetchTangledRepos(): Promise<TangledReposData | null> { 24 - const cacheKey = `tangled:${PUBLIC_ATPROTO_DID}`; 25 - const cached = cache.get<TangledReposData>(cacheKey); 26 - if (cached) return cached; 27 - 28 - try { 29 - // Custom collection, prefer PDS first 30 - const records = await withFallback( 31 - PUBLIC_ATPROTO_DID, 32 - async (agent) => { 33 - const response = await agent.com.atproto.repo.listRecords({ 34 - repo: PUBLIC_ATPROTO_DID, 35 - collection: 'sh.tangled.repo', 36 - limit: 100 37 - }); 38 - return response.data.records; 39 - }, 40 - true 41 - ); // usePDSFirst = true 42 - 43 - if (records.length === 0) return null; 44 - 45 - const repos: TangledRepo[] = records.map((record) => { 46 - const value = record.value as any; 47 - return { 48 - uri: record.uri, 49 - name: value.name, 50 - description: value.description, 51 - knot: value.knot, 52 - createdAt: value.createdAt, 53 - labels: value.labels, 54 - source: value.source, 55 - spindle: value.spindle 56 - }; 57 - }); 58 - 59 - // Sort by creation date, newest first 60 - repos.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 61 - 62 - const data: TangledReposData = { repos }; 63 - cache.set(cacheKey, data); 64 - return data; 65 - } catch (error) { 66 - console.error('Failed to fetch Tangled repos from all sources:', error); 67 - return null; 68 - } 69 - }
+15
src/lib/services/atproto/types.ts
··· 223 223 createdAt: string; 224 224 $type: 'social.kibun.status'; 225 225 } 226 + 227 + export interface TangledRepo { 228 + uri: string; 229 + name: string; 230 + description?: string; 231 + knot: string; 232 + createdAt: string; 233 + labels?: string[]; 234 + source?: string; 235 + spindle?: string; 236 + } 237 + 238 + export interface TangledReposData { 239 + repos: TangledRepo[]; 240 + }