tangled
alpha
login
or
join now
baileytownsend.dev
/
at-poke
forked from
baileytownsend.dev/atproto-sveltekit-template
1
fork
atom
WIP: Another at:// production from me
1
fork
atom
overview
issues
pulls
pipelines
wip
baileytownsend.dev
2 months ago
cc6f0875
48ff829e
+120
-54
5 changed files
expand all
collapse all
unified
split
src
lib
components
LeafletDocumentCard.svelte
MusicPlayCard.svelte
TangledRepoCard.svelte
routes
feed
+page.server.ts
+page.svelte
+26
-17
src/lib/components/LeafletDocumentCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
4
-
let { record } = $props();
4
4
+
let {
5
5
+
record,
6
6
+
profile
7
7
+
}: {
8
8
+
record: any;
9
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
title?: string;
···
53
59
<div class="card-body">
54
60
<div class="flex items-start gap-4">
55
61
<div class="flex-shrink-0">
56
56
-
<div class="avatar placeholder">
57
57
-
<div class="bg-accent text-accent-content rounded-lg w-16 h-16">
58
58
-
<svg
59
59
-
xmlns="http://www.w3.org/2000/svg"
60
60
-
class="h-8 w-8"
61
61
-
fill="none"
62
62
-
viewBox="0 0 24 24"
63
63
-
stroke="currentColor"
64
64
-
>
65
65
-
<path
66
66
-
stroke-linecap="round"
67
67
-
stroke-linejoin="round"
68
68
-
stroke-width="2"
69
69
-
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
70
70
-
/>
71
71
-
</svg>
62
62
+
<div class="avatar">
63
63
+
<div class="w-12 h-12 rounded-full">
64
64
+
{#if profile?.avatar}
65
65
+
<img src={profile.avatar} alt={profile.handle} />
66
66
+
{:else}
67
67
+
<div class="bg-accent text-accent-content rounded-full w-12 h-12 flex items-center justify-center">
68
68
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
69
69
+
</div>
70
70
+
{/if}
72
71
</div>
73
72
</div>
74
73
</div>
75
74
76
75
<div class="flex-grow">
76
76
+
{#if profile}
77
77
+
<div class="text-sm text-base-content/60 mb-2">
78
78
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
79
79
+
@{profile.handle}
80
80
+
</a>
81
81
+
{#if profile.displayName}
82
82
+
<span class="ml-1">· {profile.displayName}</span>
83
83
+
{/if}
84
84
+
</div>
85
85
+
{/if}
77
86
<h3 class="card-title text-lg mb-2">{data.title || 'Untitled Document'}</h3>
78
87
79
88
{#if data.description}
+26
-17
src/lib/components/MusicPlayCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
4
-
let { record } = $props();
4
4
+
let {
5
5
+
record,
6
6
+
profile
7
7
+
}: {
8
8
+
record: any;
9
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
trackName?: string;
···
34
40
<div class="card-body">
35
41
<div class="flex items-start gap-4">
36
42
<div class="flex-shrink-0">
37
37
-
<div class="avatar placeholder">
38
38
-
<div class="bg-primary text-primary-content rounded-lg w-16 h-16">
39
39
-
<svg
40
40
-
xmlns="http://www.w3.org/2000/svg"
41
41
-
class="h-8 w-8"
42
42
-
fill="none"
43
43
-
viewBox="0 0 24 24"
44
44
-
stroke="currentColor"
45
45
-
>
46
46
-
<path
47
47
-
stroke-linecap="round"
48
48
-
stroke-linejoin="round"
49
49
-
stroke-width="2"
50
50
-
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
51
51
-
/>
52
52
-
</svg>
43
43
+
<div class="avatar">
44
44
+
<div class="w-12 h-12 rounded-full">
45
45
+
{#if profile?.avatar}
46
46
+
<img src={profile.avatar} alt={profile.handle} />
47
47
+
{:else}
48
48
+
<div class="bg-primary text-primary-content rounded-full w-12 h-12 flex items-center justify-center">
49
49
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
50
50
+
</div>
51
51
+
{/if}
53
52
</div>
54
53
</div>
55
54
</div>
56
55
57
56
<div class="flex-grow">
57
57
+
{#if profile}
58
58
+
<div class="text-sm text-base-content/60 mb-2">
59
59
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
60
60
+
@{profile.handle}
61
61
+
</a>
62
62
+
{#if profile.displayName}
63
63
+
<span class="ml-1">· {profile.displayName}</span>
64
64
+
{/if}
65
65
+
</div>
66
66
+
{/if}
58
67
<h3 class="card-title text-lg">{data.trackName || 'Unknown Track'}</h3>
59
68
{#if data.artists && data.artists.length > 0}
60
69
<p class="text-base-content/70">
+26
-17
src/lib/components/TangledRepoCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
4
-
let { record } = $props();
4
4
+
let {
5
5
+
record,
6
6
+
profile
7
7
+
}: {
8
8
+
record: any;
9
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
name?: string;
···
31
37
<div class="card-body">
32
38
<div class="flex items-start gap-4">
33
39
<div class="flex-shrink-0">
34
34
-
<div class="avatar placeholder">
35
35
-
<div class="bg-secondary text-secondary-content rounded-lg w-16 h-16">
36
36
-
<svg
37
37
-
xmlns="http://www.w3.org/2000/svg"
38
38
-
class="h-8 w-8"
39
39
-
fill="none"
40
40
-
viewBox="0 0 24 24"
41
41
-
stroke="currentColor"
42
42
-
>
43
43
-
<path
44
44
-
stroke-linecap="round"
45
45
-
stroke-linejoin="round"
46
46
-
stroke-width="2"
47
47
-
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
48
48
-
/>
49
49
-
</svg>
40
40
+
<div class="avatar">
41
41
+
<div class="w-12 h-12 rounded-full">
42
42
+
{#if profile?.avatar}
43
43
+
<img src={profile.avatar} alt={profile.handle} />
44
44
+
{:else}
45
45
+
<div class="bg-secondary text-secondary-content rounded-full w-12 h-12 flex items-center justify-center">
46
46
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
47
47
+
</div>
48
48
+
{/if}
50
49
</div>
51
50
</div>
52
51
</div>
53
52
54
53
<div class="flex-grow">
54
54
+
{#if profile}
55
55
+
<div class="text-sm text-base-content/60 mb-2">
56
56
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
57
57
+
@{profile.handle}
58
58
+
</a>
59
59
+
{#if profile.displayName}
60
60
+
<span class="ml-1">· {profile.displayName}</span>
61
61
+
{/if}
62
62
+
</div>
63
63
+
{/if}
55
64
<div class="flex items-center gap-2">
56
65
<h3 class="card-title text-lg">{data.name || 'Unknown Repository'}</h3>
57
66
{#if data.knot}
+39
src/routes/feed/+page.server.ts
···
4
4
import { recordsTable } from '$lib/server/db/schema';
5
5
import { desc, eq, ne } from 'drizzle-orm';
6
6
7
7
+
interface ProfileData {
8
8
+
handle: string;
9
9
+
avatar?: string;
10
10
+
displayName?: string;
11
11
+
}
12
12
+
13
13
+
async function fetchProfile(repo: string): Promise<ProfileData | null> {
14
14
+
try {
15
15
+
const response = await fetch(
16
16
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${repo}`
17
17
+
);
18
18
+
if (!response.ok) return null;
19
19
+
const data = await response.json();
20
20
+
return {
21
21
+
handle: data.handle,
22
22
+
avatar: data.avatar,
23
23
+
displayName: data.displayName
24
24
+
};
25
25
+
} catch {
26
26
+
return null;
27
27
+
}
28
28
+
}
29
29
+
7
30
export const load: PageServerLoad = async (event) => {
8
31
if (!event.locals.session) {
9
32
return redirect(302, '/login');
···
30
53
(a, b) => b.indexedAt.getTime() - a.indexedAt.getTime()
31
54
);
32
55
56
56
+
// Fetch profile data for all unique repos
57
57
+
const uniqueRepos = [...new Set(records.map((r) => r.repo))];
58
58
+
const profilePromises = uniqueRepos.map(async (repo) => {
59
59
+
const profile = await fetchProfile(repo);
60
60
+
return { repo, profile };
61
61
+
});
62
62
+
63
63
+
const profileResults = await Promise.all(profilePromises);
64
64
+
const profiles: Record<string, ProfileData> = {};
65
65
+
for (const { repo, profile } of profileResults) {
66
66
+
if (profile) {
67
67
+
profiles[repo] = profile;
68
68
+
}
69
69
+
}
70
70
+
33
71
return {
34
72
records,
73
73
+
profiles,
35
74
usersDid: event.locals.session.did
36
75
};
37
76
};
+3
-3
src/routes/feed/+page.svelte
···
38
38
<div class="space-y-4">
39
39
{#each data.records as record (record.id)}
40
40
{#if record.collection === 'fm.teal.alpha.feed.play'}
41
41
-
<MusicPlayCard {record} />
41
41
+
<MusicPlayCard {record} profile={data.profiles[record.repo]} />
42
42
{:else if record.collection === 'sh.tangled.repo'}
43
43
-
<TangledRepoCard {record} />
43
43
+
<TangledRepoCard {record} profile={data.profiles[record.repo]} />
44
44
{:else if record.collection === 'pub.leaflet.document'}
45
45
-
<LeafletDocumentCard {record} />
45
45
+
<LeafletDocumentCard {record} profile={data.profiles[record.repo]} />
46
46
{:else}
47
47
<div class="card bg-base-100 shadow-xl">
48
48
<div class="card-body">