+27
-18
src/lib/components/LeafletDocumentCard.svelte
+27
-18
src/lib/components/LeafletDocumentCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
-
let { record } = $props();
4
+
let {
5
+
record,
6
+
profile
7
+
}: {
8
+
record: any;
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
title?: string;
···
49
55
const previewText = getPreviewText();
50
56
</script>
51
57
52
-
<div class="card bg-base-100 shadow-xl">
58
+
<div class="card bg-base-100 border-b border-base-300">
53
59
<div class="card-body">
54
60
<div class="flex items-start gap-4">
55
61
<div class="flex-shrink-0">
56
-
<div class="avatar placeholder">
57
-
<div class="bg-accent text-accent-content rounded-lg w-16 h-16">
58
-
<svg
59
-
xmlns="http://www.w3.org/2000/svg"
60
-
class="h-8 w-8"
61
-
fill="none"
62
-
viewBox="0 0 24 24"
63
-
stroke="currentColor"
64
-
>
65
-
<path
66
-
stroke-linecap="round"
67
-
stroke-linejoin="round"
68
-
stroke-width="2"
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
-
/>
71
-
</svg>
62
+
<div class="avatar">
63
+
<div class="w-12 h-12 rounded-full">
64
+
{#if profile?.avatar}
65
+
<img src={profile.avatar} alt={profile.handle} />
66
+
{:else}
67
+
<div class="bg-accent text-accent-content rounded-full w-12 h-12 flex items-center justify-center">
68
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
69
+
</div>
70
+
{/if}
72
71
</div>
73
72
</div>
74
73
</div>
75
74
76
75
<div class="flex-grow">
76
+
{#if profile}
77
+
<div class="text-sm text-base-content/60 mb-2">
78
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
79
+
@{profile.handle}
80
+
</a>
81
+
{#if profile.displayName}
82
+
<span class="ml-1">ยท {profile.displayName}</span>
83
+
{/if}
84
+
</div>
85
+
{/if}
77
86
<h3 class="card-title text-lg mb-2">{data.title || 'Untitled Document'}</h3>
78
87
79
88
{#if data.description}
+27
-18
src/lib/components/MusicPlayCard.svelte
+27
-18
src/lib/components/MusicPlayCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
-
let { record } = $props();
4
+
let {
5
+
record,
6
+
profile
7
+
}: {
8
+
record: any;
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
trackName?: string;
···
30
36
}
31
37
</script>
32
38
33
-
<div class="card bg-base-100 shadow-xl">
39
+
<div class="card bg-base-100 border-b border-base-300">
34
40
<div class="card-body">
35
41
<div class="flex items-start gap-4">
36
42
<div class="flex-shrink-0">
37
-
<div class="avatar placeholder">
38
-
<div class="bg-primary text-primary-content rounded-lg w-16 h-16">
39
-
<svg
40
-
xmlns="http://www.w3.org/2000/svg"
41
-
class="h-8 w-8"
42
-
fill="none"
43
-
viewBox="0 0 24 24"
44
-
stroke="currentColor"
45
-
>
46
-
<path
47
-
stroke-linecap="round"
48
-
stroke-linejoin="round"
49
-
stroke-width="2"
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
-
/>
52
-
</svg>
43
+
<div class="avatar">
44
+
<div class="w-12 h-12 rounded-full">
45
+
{#if profile?.avatar}
46
+
<img src={profile.avatar} alt={profile.handle} />
47
+
{:else}
48
+
<div class="bg-primary text-primary-content rounded-full w-12 h-12 flex items-center justify-center">
49
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
50
+
</div>
51
+
{/if}
53
52
</div>
54
53
</div>
55
54
</div>
56
55
57
56
<div class="flex-grow">
57
+
{#if profile}
58
+
<div class="text-sm text-base-content/60 mb-2">
59
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
60
+
@{profile.handle}
61
+
</a>
62
+
{#if profile.displayName}
63
+
<span class="ml-1">ยท {profile.displayName}</span>
64
+
{/if}
65
+
</div>
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">
+27
-18
src/lib/components/TangledRepoCard.svelte
+27
-18
src/lib/components/TangledRepoCard.svelte
···
1
1
<script lang="ts">
2
2
import InteractionBar from './InteractionBar.svelte';
3
3
4
-
let { record } = $props();
4
+
let {
5
+
record,
6
+
profile
7
+
}: {
8
+
record: any;
9
+
profile?: { handle: string; avatar?: string; displayName?: string };
10
+
} = $props();
5
11
6
12
const data = $derived(record.data as {
7
13
name?: string;
···
27
33
}
28
34
</script>
29
35
30
-
<div class="card bg-base-100 shadow-xl">
36
+
<div class="card bg-base-100 border-b border-base-300">
31
37
<div class="card-body">
32
38
<div class="flex items-start gap-4">
33
39
<div class="flex-shrink-0">
34
-
<div class="avatar placeholder">
35
-
<div class="bg-secondary text-secondary-content rounded-lg w-16 h-16">
36
-
<svg
37
-
xmlns="http://www.w3.org/2000/svg"
38
-
class="h-8 w-8"
39
-
fill="none"
40
-
viewBox="0 0 24 24"
41
-
stroke="currentColor"
42
-
>
43
-
<path
44
-
stroke-linecap="round"
45
-
stroke-linejoin="round"
46
-
stroke-width="2"
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
-
/>
49
-
</svg>
40
+
<div class="avatar">
41
+
<div class="w-12 h-12 rounded-full">
42
+
{#if profile?.avatar}
43
+
<img src={profile.avatar} alt={profile.handle} />
44
+
{:else}
45
+
<div class="bg-secondary text-secondary-content rounded-full w-12 h-12 flex items-center justify-center">
46
+
<span class="text-xl">{profile?.handle?.[0]?.toUpperCase() || '?'}</span>
47
+
</div>
48
+
{/if}
50
49
</div>
51
50
</div>
52
51
</div>
53
52
54
53
<div class="flex-grow">
54
+
{#if profile}
55
+
<div class="text-sm text-base-content/60 mb-2">
56
+
<a href="https://bsky.app/profile/{profile.handle}" target="_blank" rel="noopener noreferrer" class="link link-hover">
57
+
@{profile.handle}
58
+
</a>
59
+
{#if profile.displayName}
60
+
<span class="ml-1">ยท {profile.displayName}</span>
61
+
{/if}
62
+
</div>
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
+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
+
interface ProfileData {
8
+
handle: string;
9
+
avatar?: string;
10
+
displayName?: string;
11
+
}
12
+
13
+
async function fetchProfile(repo: string): Promise<ProfileData | null> {
14
+
try {
15
+
const response = await fetch(
16
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${repo}`
17
+
);
18
+
if (!response.ok) return null;
19
+
const data = await response.json();
20
+
return {
21
+
handle: data.handle,
22
+
avatar: data.avatar,
23
+
displayName: data.displayName
24
+
};
25
+
} catch {
26
+
return null;
27
+
}
28
+
}
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
+
// Fetch profile data for all unique repos
57
+
const uniqueRepos = [...new Set(records.map((r) => r.repo))];
58
+
const profilePromises = uniqueRepos.map(async (repo) => {
59
+
const profile = await fetchProfile(repo);
60
+
return { repo, profile };
61
+
});
62
+
63
+
const profileResults = await Promise.all(profilePromises);
64
+
const profiles: Record<string, ProfileData> = {};
65
+
for (const { repo, profile } of profileResults) {
66
+
if (profile) {
67
+
profiles[repo] = profile;
68
+
}
69
+
}
70
+
33
71
return {
34
72
records,
73
+
profiles,
35
74
usersDid: event.locals.session.did
36
75
};
37
76
};
+5
-5
src/routes/feed/+page.svelte
+5
-5
src/routes/feed/+page.svelte
···
35
35
<span>No records found. The feed is empty.</span>
36
36
</div>
37
37
{:else}
38
-
<div class="space-y-4">
38
+
<div>
39
39
{#each data.records as record (record.id)}
40
40
{#if record.collection === 'fm.teal.alpha.feed.play'}
41
-
<MusicPlayCard {record} />
41
+
<MusicPlayCard {record} profile={data.profiles[record.repo]} />
42
42
{:else if record.collection === 'sh.tangled.repo'}
43
-
<TangledRepoCard {record} />
43
+
<TangledRepoCard {record} profile={data.profiles[record.repo]} />
44
44
{:else if record.collection === 'pub.leaflet.document'}
45
-
<LeafletDocumentCard {record} />
45
+
<LeafletDocumentCard {record} profile={data.profiles[record.repo]} />
46
46
{:else}
47
-
<div class="card bg-base-100 shadow-xl">
47
+
<div class="card bg-base-100 border-b border-base-300">
48
48
<div class="card-body">
49
49
<h3 class="card-title text-sm opacity-60">Unknown Collection Type</h3>
50
50
<p class="text-sm">{record.collection}</p>