+1
-1
README.md
+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
-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
+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
src/lib/components/layout/main/index.ts
+55
-1
src/lib/services/atproto/fetch.ts
+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
+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
-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
+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
+
}