tangled
alpha
login
or
join now
flo-bit.dev
/
blento
21
fork
atom
your personal website on atproto - mirror
blento.app
21
fork
atom
overview
issues
pulls
pipelines
cards as embeds
Florian
1 week ago
0a5e3804
8af94dac
+750
-163
19 changed files
expand all
collapse all
unified
split
src
lib
cards
_base
BaseCard
BaseCard.svelte
core
LinkCard
EditingLinkCard.svelte
LinkCard.svelte
LinkCardSettings.svelte
media
LivestreamCard
LivestreamCard.svelte
PhotoGalleryCard
PhotoGalleryCard.svelte
RockskyPlaysCard
RockskyPlaysCard.svelte
TealFMPlaysCard
TealFMPlaysCard.svelte
social
EventCard
EventCard.svelte
FriendsCard
FriendsCard.svelte
GitHubProfileCard
GitHubProfileCard.svelte
UpcomingEventsCard
UpcomingEventsCard.svelte
website
EmbeddedCard.svelte
load.ts
routes
+layout.svelte
[[actor=actor]]
card
[rkey]
+page.server.ts
+page.svelte
embed
type
[type]
+page.server.ts
+page.svelte
+18
-4
src/lib/cards/_base/BaseCard/BaseCard.svelte
···
18
18
isEditing?: boolean;
19
19
showOutline?: boolean;
20
20
locked?: boolean;
21
21
+
fillPage?: boolean;
21
22
} & WithElementRef<HTMLAttributes<HTMLDivElement>>;
22
23
23
24
let {
···
28
29
controls,
29
30
showOutline,
30
31
locked = false,
32
32
+
fillPage = false,
31
33
class: className,
32
34
...rest
33
35
}: BaseCardProps = $props();
···
38
40
<div
39
41
id={item.id}
40
42
data-flip-id={item.id}
43
43
+
data-fill-page={fillPage ? 'true' : undefined}
41
44
bind:this={ref}
42
45
draggable={false}
43
46
class={[
44
44
-
'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2',
45
45
-
color ? (colors[color] ?? colors.accent) : colors.base,
47
47
+
fillPage
48
48
+
? 'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card relative isolate z-0 h-full w-full outline-offset-2 transition-all duration-200 focus-within:outline-2'
49
49
+
: 'card group/card selection:bg-accent-600/50 focus-within:outline-accent-500 @container/card absolute isolate z-0 rounded-3xl outline-offset-2 transition-all duration-200 focus-within:outline-2',
50
50
+
!fillPage ? (color ? (colors[color] ?? colors.accent) : colors.base) : '',
46
51
color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '',
47
52
showOutline ? 'outline-2' : '',
48
53
className
···
65
70
>
66
71
<div
67
72
class={[
68
68
-
'text-base-900 dark:text-base-50 relative isolate h-full w-full overflow-hidden rounded-[23px]',
73
73
+
'text-base-900 dark:text-base-50 relative isolate h-full w-full overflow-hidden',
74
74
+
!fillPage ? 'rounded-[23px]' : '',
69
75
color !== 'base' && color != 'transparent' ? 'light' : ''
70
76
]}
71
77
>
···
84
90
85
91
<style>
86
92
.card {
93
93
+
container-name: card;
94
94
+
container-type: size;
87
95
translate: calc((var(--mx) / var(--columns)) * 100cqw + var(--mm))
88
96
calc((var(--my) / var(--columns)) * 100cqw + var(--mm));
89
97
width: calc((var(--mw) / var(--columns)) * 100cqw - (var(--mm) * 2));
90
98
height: calc((var(--mh) / var(--columns)) * 100cqw - (var(--mm) * 2));
91
99
}
92
100
101
101
+
.card[data-fill-page='true'] {
102
102
+
translate: none;
103
103
+
width: 100%;
104
104
+
height: 100%;
105
105
+
}
106
106
+
93
107
@container grid (width >= 42rem) {
94
94
-
.card {
108
108
+
.card:not([data-fill-page='true']) {
95
109
translate: calc((var(--dx) / var(--columns)) * 100cqw + var(--dm))
96
110
calc((var(--dy) / var(--columns)) * 100cqw + var(--dm));
97
111
width: calc((var(--dw) / var(--columns)) * 100cqw - (var(--dm) * 2));
+15
-5
src/lib/cards/core/LinkCard/EditingLinkCard.svelte
···
1
1
<script lang="ts">
2
2
import { browser } from '$app/environment';
3
3
import { getImage, compressImage } from '$lib/helper';
4
4
-
import { getDidContext, getIsMobile } from '$lib/website/context';
4
4
+
import { getDidContext } from '$lib/website/context';
5
5
import type { ContentComponentProps } from '../../types';
6
6
import PlainTextEditor from '$lib/components/PlainTextEditor.svelte';
7
7
···
49
49
console.error('Failed to process image:', error);
50
50
}
51
51
}
52
52
-
53
53
-
let isMobile = getIsMobile();
54
52
55
53
let faviconHasError = $state(false);
56
54
let isFetchingMetadata = $state(false);
···
291
289
</div>
292
290
</div>
293
291
294
294
-
{#if hasFetched && browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4))}
292
292
+
{#if hasFetched && browser}
295
293
<button
296
294
type="button"
297
297
-
class="hover:ring-accent-500 relative mb-2 aspect-2/1 w-full cursor-pointer overflow-hidden rounded-xl transition-all duration-200 hover:ring-2"
295
295
+
class="link-preview-editor hover:ring-accent-500 relative mb-2 aspect-2/1 w-full cursor-pointer overflow-hidden rounded-xl transition-all duration-200 hover:ring-2"
298
296
onclick={() => imageInputRef?.click()}
299
297
onmouseenter={() => (isHoveringImage = true)}
300
298
onmouseleave={() => (isHoveringImage = false)}
···
357
355
{/if}
358
356
</div>
359
357
{/if}
358
358
+
359
359
+
<style>
360
360
+
.link-preview-editor {
361
361
+
display: none;
362
362
+
}
363
363
+
364
364
+
@container card (height >= 18rem) {
365
365
+
.link-preview-editor {
366
366
+
display: block;
367
367
+
}
368
368
+
}
369
369
+
</style>
+79
-25
src/lib/cards/core/LinkCard/LinkCard.svelte
···
1
1
<script lang="ts">
2
2
-
import { browser } from '$app/environment';
3
2
import { getImage } from '$lib/helper';
4
4
-
import { getDidContext, getIsMobile } from '$lib/website/context';
3
3
+
import { getDidContext } from '$lib/website/context';
5
4
import type { ContentComponentProps } from '../../types';
6
5
import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte';
7
6
8
7
let { item, isEditing }: ContentComponentProps = $props();
9
8
10
10
-
let isMobile = getIsMobile();
11
11
-
12
9
let faviconHasError = $state(false);
13
10
14
11
let did = getDidContext();
15
12
</script>
16
13
17
14
{#if item.cardData.showBackgroundImage && item.cardData.image}
18
18
-
<div class="relative flex h-full flex-col justify-end p-4">
15
15
+
<div class="link-card relative flex h-full flex-col justify-end p-4">
19
16
<img
20
17
class="absolute inset-0 -z-10 size-full object-cover"
21
18
src={getImage(item.cardData, did)}
···
27
24
<div class="text-accent-600 dark:text-accent-400 text-xs font-semibold">
28
25
{item.cardData.domain}
29
26
</div>
30
30
-
<div
31
31
-
class={[
32
32
-
'text-base-900 dark:text-base-50 text-lg font-bold',
33
33
-
(isMobile() && item.mobileH < 8) || (!isMobile() && item.h < 4) ? 'line-clamp-2' : ''
34
34
-
]}
35
35
-
>
27
27
+
<div class="link-title text-base-900 dark:text-base-50 text-lg font-bold">
36
28
{item.cardData.title}
37
29
</div>
38
30
{#if item.cardData.href && !isEditing}
···
73
65
{/if}
74
66
</div>
75
67
{:else}
76
76
-
<div class="flex h-full flex-col justify-between p-4">
77
77
-
<div>
68
68
+
<div class="link-card flex h-full flex-col p-4">
69
69
+
<div class="link-content min-h-0">
78
70
<div
79
71
class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 mb-2 inline-flex size-8 items-center justify-center rounded-xl border"
80
72
>
···
102
94
</svg>
103
95
{/if}
104
96
</div>
105
105
-
<div
106
106
-
class={[
107
107
-
'text-base-900 dark:text-base-50 text-lg font-bold',
108
108
-
(isMobile() && item.mobileH < 8) || (!isMobile() && item.h < 4) ? 'line-clamp-2' : ''
109
109
-
]}
110
110
-
>
97
97
+
<div class="link-title text-base-900 dark:text-base-50 text-lg font-bold">
111
98
{item.cardData.title}
112
99
</div>
113
100
<!-- <div class="text-base-800 dark:text-base-100 mt-2 text-xs">{item.cardData.description}</div> -->
···
118
105
</div>
119
106
</div>
120
107
121
121
-
{#if browser && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) && item.cardData.image}
122
122
-
<img
123
123
-
class="mb-2 aspect-2/1 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
124
124
-
src={getImage(item.cardData, did)}
125
125
-
alt=""
126
126
-
/>
108
108
+
{#if item.cardData.image}
109
109
+
<div class="link-preview-wrap mt-auto">
110
110
+
<img
111
111
+
class="link-preview mb-2 aspect-2/1 w-full rounded-xl object-cover opacity-100 transition-opacity duration-100 starting:opacity-0"
112
112
+
src={getImage(item.cardData, did)}
113
113
+
alt=""
114
114
+
/>
115
115
+
</div>
127
116
{/if}
128
117
{#if item.cardData.href && !isEditing}
129
118
<a
···
163
152
{/if}
164
153
</div>
165
154
{/if}
155
155
+
156
156
+
<style>
157
157
+
.link-title {
158
158
+
display: -webkit-box;
159
159
+
line-clamp: 2;
160
160
+
overflow: hidden;
161
161
+
-webkit-box-orient: vertical;
162
162
+
-webkit-line-clamp: 2;
163
163
+
}
164
164
+
165
165
+
.link-preview {
166
166
+
display: none;
167
167
+
width: 100%;
168
168
+
object-fit: cover;
169
169
+
}
170
170
+
171
171
+
.link-preview-wrap {
172
172
+
display: none;
173
173
+
padding-top: 1rem;
174
174
+
}
175
175
+
176
176
+
@container card (height >= 18rem) {
177
177
+
.link-title {
178
178
+
display: block;
179
179
+
line-clamp: unset;
180
180
+
overflow: visible;
181
181
+
-webkit-line-clamp: unset;
182
182
+
}
183
183
+
184
184
+
.link-preview-wrap,
185
185
+
.link-preview {
186
186
+
display: block;
187
187
+
}
188
188
+
}
189
189
+
190
190
+
@container card (height >= 18rem) and (height < 22rem) {
191
191
+
.link-content {
192
192
+
padding-bottom: 1rem;
193
193
+
}
194
194
+
195
195
+
.link-preview-wrap {
196
196
+
padding-top: 0;
197
197
+
}
198
198
+
199
199
+
.link-preview {
200
200
+
aspect-ratio: 2.6 / 1;
201
201
+
max-height: 4.5rem;
202
202
+
}
203
203
+
}
204
204
+
205
205
+
@container card (height >= 22rem) {
206
206
+
.link-content {
207
207
+
padding-bottom: 0.5rem;
208
208
+
}
209
209
+
210
210
+
.link-preview-wrap {
211
211
+
padding-top: 0;
212
212
+
}
213
213
+
214
214
+
.link-preview {
215
215
+
aspect-ratio: 2 / 1;
216
216
+
max-height: none;
217
217
+
}
218
218
+
}
219
219
+
</style>
+2
-2
src/lib/cards/core/LinkCard/LinkCardSettings.svelte
···
48
48
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
49
49
</svg>
50
50
</Button>
51
51
-
<div class="flex items-center space-x- mt-4">
51
51
+
<div class="space-x- mt-4 flex items-center">
52
52
<Checkbox
53
53
bind:checked={
54
54
() => Boolean(item.cardData.showBackgroundImage),
···
61
61
<Label
62
62
id="show-bg-image-label"
63
63
for="show-bg-image"
64
64
-
class="text-sm leading-none ml-2 font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
64
64
+
class="ml-2 text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
65
65
>
66
66
Show background image
67
67
</Label>
+16
-11
src/lib/cards/media/LivestreamCard/LivestreamCard.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
import Icon from './Icon.svelte';
4
4
-
import {
5
5
-
getAdditionalUserData,
6
6
-
getDidContext,
7
7
-
getHandleContext,
8
8
-
getIsMobile
9
9
-
} from '$lib/website/context';
4
4
+
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
10
5
import type { ContentComponentProps } from '../../types';
11
6
import { RelativeTime } from '@foxui/time';
12
7
import { Badge } from '@foxui/core';
···
15
10
16
11
let { item = $bindable() }: ContentComponentProps = $props();
17
12
18
18
-
let isMobile = getIsMobile();
19
19
-
20
13
let isLoaded = $state(false);
21
14
22
15
const data = getAdditionalUserData();
···
58
51
});
59
52
</script>
60
53
61
61
-
<div class="h-full overflow-y-scroll p-4">
54
54
+
<div class="livestream-card h-full overflow-y-scroll p-4">
62
55
{#if latestLivestream}
63
56
<div class="flex min-h-full flex-col justify-between">
64
57
<div>
···
95
88
</a>
96
89
</div>
97
90
98
98
-
{#if browser && ((isMobile() && item.mobileH >= 7) || (!isMobile() && item.h >= 4)) && latestLivestream?.thumb}
91
91
+
{#if browser && latestLivestream?.thumb}
99
92
<a href={latestLivestream?.href} target="_blank" rel="noopener noreferrer">
100
93
<img
101
101
-
class="my-4 max-h-32 w-full rounded-xl object-cover"
94
94
+
class="livestream-thumb my-4 max-h-32 w-full rounded-xl object-cover"
102
95
src={latestLivestream?.thumb}
103
96
alt=""
104
97
/>
···
112
105
<div class="flex h-full w-full items-center justify-center">Looking for the latest stream</div>
113
106
{/if}
114
107
</div>
108
108
+
109
109
+
<style>
110
110
+
.livestream-thumb {
111
111
+
display: none;
112
112
+
}
113
113
+
114
114
+
@container card (height >= 15rem) {
115
115
+
.livestream-thumb {
116
116
+
display: block;
117
117
+
}
118
118
+
}
119
119
+
</style>
+24
-9
src/lib/cards/media/PhotoGalleryCard/PhotoGalleryCard.svelte
···
1
1
<script lang="ts">
2
2
import type { Item } from '$lib/types';
3
3
import { onMount } from 'svelte';
4
4
-
import { getAdditionalUserData, getIsMobile } from '$lib/website/context';
4
4
+
import { getAdditionalUserData } from '$lib/website/context';
5
5
import { getCDNImageBlobUrl, parseUri } from '$lib/atproto';
6
6
import { loadGrainGalleryData } from './helpers';
7
7
···
61
61
onclick?: () => void;
62
62
}[]
63
63
);
64
64
-
65
65
-
let isMobile = getIsMobile();
66
64
</script>
67
65
68
68
-
<div class="z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4">
69
69
-
<ImageMasonry
70
70
-
images={images ?? []}
71
71
-
showNames={false}
72
72
-
maxColumns={!isMobile() && item.w > 4 ? 3 : 2}
73
73
-
/>
66
66
+
<div class="photo-gallery-card z-10 flex h-full w-full flex-col gap-4 overflow-y-scroll p-4">
67
67
+
<div class="gallery-compact">
68
68
+
<ImageMasonry images={images ?? []} showNames={false} maxColumns={2} />
69
69
+
</div>
70
70
+
<div class="gallery-wide">
71
71
+
<ImageMasonry images={images ?? []} showNames={false} maxColumns={3} />
72
72
+
</div>
74
73
</div>
74
74
+
75
75
+
<style>
76
76
+
.gallery-wide {
77
77
+
display: none;
78
78
+
}
79
79
+
80
80
+
@container card (width >= 28rem) {
81
81
+
.gallery-compact {
82
82
+
display: none;
83
83
+
}
84
84
+
85
85
+
.gallery-wide {
86
86
+
display: block;
87
87
+
}
88
88
+
}
89
89
+
</style>
+1
src/lib/cards/media/RockskyPlaysCard/RockskyPlaysCard.svelte
···
8
8
9
9
interface Artist {
10
10
artist: string;
11
11
+
name?: string;
11
12
}
12
13
13
14
interface PlayValue {
+2
-1
src/lib/cards/media/TealFMPlaysCard/TealFMPlaysCard.svelte
···
12
12
13
13
interface PlayValue {
14
14
releaseMbId?: string;
15
15
+
releaseMbid?: string;
15
16
trackName: string;
16
17
playedTime?: string;
17
18
artists?: Artist[];
···
52
53
{#snippet musicItem(play: Play)}
53
54
<div class="flex w-full items-center gap-3">
54
55
<div class="size-10 shrink-0">
55
55
-
<AlbumArt releaseMbid={play.value.releaseMbid} alt="" />
56
56
+
<AlbumArt releaseMbid={play.value.releaseMbid ?? play.value.releaseMbId} alt="" />
56
57
</div>
57
58
<div class="min-w-0 flex-1">
58
59
<div class="inline-flex w-full max-w-full justify-between gap-2">
+41
-12
src/lib/cards/social/EventCard/EventCard.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
import { Badge, Button } from '@foxui/core';
4
4
-
import { getAdditionalUserData, getIsMobile } from '$lib/website/context';
4
4
+
import { getAdditionalUserData } from '$lib/website/context';
5
5
import type { ContentComponentProps } from '../../types';
6
6
import { CardDefinitionsByType } from '../..';
7
7
import type { EventData } from '.';
···
12
12
13
13
let { item }: ContentComponentProps = $props();
14
14
15
15
-
let isMobile = getIsMobile();
16
15
let isLoaded = $state(false);
17
16
let fetchedEventData = $state<EventData | undefined>(undefined);
18
17
···
109
108
};
110
109
});
111
110
112
112
-
let showImage = $derived(
113
113
-
browser && headerImage() && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4))
114
114
-
);
111
111
+
let showImage = $derived(browser && headerImage());
115
112
</script>
116
113
117
117
-
<div class="flex h-full flex-col justify-between overflow-hidden p-4">
114
114
+
<div class="event-card flex h-full flex-col justify-between overflow-hidden p-4">
118
115
{#if eventData}
119
116
<div class="min-w-0 flex-1 overflow-hidden">
120
117
<div class="mb-2 flex items-center justify-between gap-2">
···
142
139
</Badge>
143
140
</div>
144
141
145
145
-
{#if isMobile() ? item.mobileW > 4 : item.w > 2}
146
146
-
<Button href={eventUrl()} target="_blank" class="z-50">View event</Button>
147
147
-
{/if}
142
142
+
<div class="event-action z-50">
143
143
+
<Button href={eventUrl()} target="_blank">View event</Button>
144
144
+
</div>
148
145
</div>
149
146
150
147
<h3 class="text-base-900 dark:text-base-50 mb-2 line-clamp-2 text-lg leading-tight font-bold">
···
203
200
</div>
204
201
{/if}
205
202
206
206
-
{#if eventData.description && ((isMobile() && item.mobileH >= 5) || (!isMobile() && item.h >= 3))}
207
207
-
<p class="text-base-500 dark:text-base-400 accent:text-base-900 mb-3 line-clamp-3 text-sm">
203
203
+
{#if eventData.description}
204
204
+
<p
205
205
+
class="event-description text-base-500 dark:text-base-400 accent:text-base-900 mb-3 line-clamp-3 text-sm"
206
206
+
>
208
207
{eventData.description}
209
208
</p>
210
209
{/if}
···
213
212
{#if showImage}
214
213
{@const img = headerImage()}
215
214
{#if img}
216
216
-
<img src={img.url} alt={img.alt} class="mt-3 aspect-3/1 w-full rounded-xl object-cover" />
215
215
+
<img
216
216
+
src={img.url}
217
217
+
alt={img.alt}
218
218
+
class="event-image mt-3 aspect-3/1 w-full rounded-xl object-cover"
219
219
+
/>
217
220
{/if}
218
221
{/if}
219
222
···
239
242
</div>
240
243
{/if}
241
244
</div>
245
245
+
246
246
+
<style>
247
247
+
.event-action,
248
248
+
.event-description,
249
249
+
.event-image {
250
250
+
display: none;
251
251
+
}
252
252
+
253
253
+
@container card (width >= 18rem) {
254
254
+
.event-action {
255
255
+
display: inline-flex;
256
256
+
}
257
257
+
}
258
258
+
259
259
+
@container card (height >= 12rem) {
260
260
+
.event-description {
261
261
+
display: block;
262
262
+
}
263
263
+
}
264
264
+
265
265
+
@container card (height >= 15rem) {
266
266
+
.event-image {
267
267
+
display: block;
268
268
+
}
269
269
+
}
270
270
+
</style>
+42
-19
src/lib/cards/social/FriendsCard/FriendsCard.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
import type { ContentComponentProps } from '../../types';
4
4
-
import { getAdditionalUserData, getCanEdit, getIsMobile } from '$lib/website/context';
4
4
+
import { getAdditionalUserData, getCanEdit } from '$lib/website/context';
5
5
import { getBlentoOrBskyProfile } from '$lib/atproto/methods';
6
6
import type { FriendsProfile } from '.';
7
7
import type { Did } from '@atcute/lexicons';
···
9
9
10
10
let { item }: ContentComponentProps = $props();
11
11
12
12
-
const isMobile = getIsMobile();
13
12
const canEdit = getCanEdit();
14
13
const additionalData = getAdditionalUserData();
15
14
···
54
53
}
55
54
});
56
55
57
57
-
let sizeClass = $derived.by(() => {
58
58
-
const w = isMobile() ? item.mobileW / 2 : item.w;
59
59
-
if (w < 3) return 'sm';
60
60
-
if (w < 5) return 'md';
61
61
-
return 'lg';
62
62
-
});
63
63
-
64
56
function removeFriend(did: string) {
65
57
item.cardData.friends = item.cardData.friends.filter((d: string) => d !== did);
66
58
}
···
84
76
</span>
85
77
{/if}
86
78
{:else}
87
87
-
{@const olX = sizeClass === 'sm' ? 12 : sizeClass === 'md' ? 20 : 24}
88
88
-
{@const olY = sizeClass === 'sm' ? 8 : sizeClass === 'md' ? 12 : 16}
89
89
-
<div class="">
90
90
-
<div class="flex flex-wrap items-center justify-center" style="padding: {olY}px 0 0 {olX}px;">
79
79
+
<div class="friends-card">
80
80
+
<div class="friends-grid flex flex-wrap items-center justify-center">
91
81
{#each profiles as profile (profile.did)}
92
92
-
<div class="group relative" style="margin: -{olY}px 0 0 -{olX}px;">
82
82
+
<div class="friends-avatar group relative">
93
83
<a
94
84
href={getLink(profile)}
95
85
class="accent:ring-accent-500 relative block rounded-full ring-2 ring-white transition-transform hover:scale-110 dark:ring-neutral-900"
96
86
>
97
97
-
<Avatar
98
98
-
src={profile.avatar}
99
99
-
alt={profile.handle}
100
100
-
class={sizeClass === 'sm' ? 'size-12' : sizeClass === 'md' ? 'size-16' : 'size-20'}
101
101
-
/>
87
87
+
<Avatar src={profile.avatar} alt={profile.handle} class="friends-avatar-image" />
102
88
</a>
103
89
{#if canEdit()}
104
90
<button
···
128
114
</div>
129
115
{/if}
130
116
</div>
117
117
+
118
118
+
<style>
119
119
+
.friends-card {
120
120
+
--friends-overlap-x: 12px;
121
121
+
--friends-overlap-y: 8px;
122
122
+
--friends-avatar-size: 48px;
123
123
+
}
124
124
+
125
125
+
.friends-grid {
126
126
+
padding: var(--friends-overlap-y) 0 0 var(--friends-overlap-x);
127
127
+
}
128
128
+
129
129
+
.friends-avatar {
130
130
+
margin: calc(var(--friends-overlap-y) * -1) 0 0 calc(var(--friends-overlap-x) * -1);
131
131
+
}
132
132
+
133
133
+
:global(.friends-avatar-image) {
134
134
+
width: var(--friends-avatar-size);
135
135
+
height: var(--friends-avatar-size);
136
136
+
}
137
137
+
138
138
+
@container card (width >= 18rem) {
139
139
+
.friends-card {
140
140
+
--friends-overlap-x: 20px;
141
141
+
--friends-overlap-y: 12px;
142
142
+
--friends-avatar-size: 64px;
143
143
+
}
144
144
+
}
145
145
+
146
146
+
@container card (width >= 26rem) {
147
147
+
.friends-card {
148
148
+
--friends-overlap-x: 24px;
149
149
+
--friends-overlap-y: 16px;
150
150
+
--friends-avatar-size: 80px;
151
151
+
}
152
152
+
}
153
153
+
</style>
+40
-12
src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
import { siGithub } from 'simple-icons';
4
4
-
import { getAdditionalUserData, getIsMobile } from '$lib/website/context';
4
4
+
import { getAdditionalUserData } from '$lib/website/context';
5
5
import type { ContentComponentProps } from '../../types';
6
6
import type { GithubProfileLoadedData } from '.';
7
7
import GithubContributionsGraph from './GithubContributionsGraph.svelte';
···
34
34
}
35
35
}
36
36
});
37
37
-
38
38
-
let isMobile = getIsMobile();
39
37
</script>
40
38
41
41
-
<div class="h-full overflow-hidden p-4">
39
39
+
<div class="github-profile-card h-full overflow-hidden p-4">
42
40
<div class="flex h-full flex-col justify-between">
43
41
<!-- Header -->
44
42
<div class="flex justify-between">
···
56
54
</a>
57
55
</div>
58
56
59
59
-
{#if isMobile() ? item.mobileW > 4 : item.w > 2}
57
57
+
<div class="github-follow z-50">
60
58
<Button
61
59
href="https://github.com/{item.cardData.user}"
62
60
target="_blank"
63
63
-
rel="noopener noreferrer"
64
64
-
class="z-50">Follow</Button
61
61
+
rel="noopener noreferrer">Follow</Button
65
62
>
66
66
-
{/if}
63
63
+
</div>
67
64
</div>
68
65
69
66
{#if contributionsData && browser}
70
67
<div class="flex opacity-100 transition-opacity duration-300 starting:opacity-0">
71
71
-
<GithubContributionsGraph
72
72
-
data={contributionsData}
73
73
-
isBig={isMobile() ? item.mobileH > 5 : item.h > 2}
74
74
-
/>
68
68
+
<div class="github-graph github-graph-compact">
69
69
+
<GithubContributionsGraph data={contributionsData} isBig={false} />
70
70
+
</div>
71
71
+
<div class="github-graph github-graph-expanded">
72
72
+
<GithubContributionsGraph data={contributionsData} isBig={true} />
73
73
+
</div>
75
74
</div>
76
75
{/if}
77
76
</div>
···
94
93
<span class="sr-only">Show on github</span>
95
94
</a>
96
95
{/if}
96
96
+
97
97
+
<style>
98
98
+
.github-follow,
99
99
+
.github-graph-expanded {
100
100
+
display: none;
101
101
+
}
102
102
+
103
103
+
.github-graph-compact {
104
104
+
display: flex;
105
105
+
width: 100%;
106
106
+
}
107
107
+
108
108
+
@container card (width >= 18rem) {
109
109
+
.github-follow {
110
110
+
display: inline-flex;
111
111
+
}
112
112
+
}
113
113
+
114
114
+
@container card (height >= 12rem) {
115
115
+
.github-graph-compact {
116
116
+
display: none;
117
117
+
}
118
118
+
119
119
+
.github-graph-expanded {
120
120
+
display: flex;
121
121
+
width: 100%;
122
122
+
}
123
123
+
}
124
124
+
</style>
+1
-7
src/lib/cards/social/UpcomingEventsCard/UpcomingEventsCard.svelte
···
1
1
<script lang="ts">
2
2
import { onMount } from 'svelte';
3
3
import { Badge } from '@foxui/core';
4
4
-
import {
5
5
-
getAdditionalUserData,
6
6
-
getDidContext,
7
7
-
getHandleContext,
8
8
-
getIsMobile
9
9
-
} from '$lib/website/context';
4
4
+
import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context';
10
5
import type { ContentComponentProps } from '../../types';
11
6
import { UpcomingEventsCardDefinition } from '.';
12
7
import type { EventData } from '../EventCard';
···
16
11
17
12
let { item }: ContentComponentProps = $props();
18
13
19
19
-
let isMobile = getIsMobile();
20
14
let isLoaded = $state(false);
21
15
const data = getAdditionalUserData();
22
16
const did = getDidContext();
+177
src/lib/website/EmbeddedCard.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { browser } from '$app/environment';
3
3
+
import { page } from '$app/state';
4
4
+
import { innerWidth } from 'svelte/reactivity/window';
5
5
+
import BaseCard from '$lib/cards/_base/BaseCard/BaseCard.svelte';
6
6
+
import Card from '$lib/cards/_base/Card/Card.svelte';
7
7
+
import { CardDefinitionsByType, getColor } from '$lib/cards';
8
8
+
import { getDescription, getImage, getName } from '$lib/helper';
9
9
+
import QRModalProvider from '$lib/components/qr/QRModalProvider.svelte';
10
10
+
import ImageViewerProvider from '$lib/components/image-viewer/ImageViewerProvider.svelte';
11
11
+
import type { WebsiteData } from '$lib/types';
12
12
+
import Context from './Context.svelte';
13
13
+
import Head from './Head.svelte';
14
14
+
import { setIsMobile } from './context';
15
15
+
16
16
+
let { data }: { data: WebsiteData } = $props();
17
17
+
18
18
+
let item = $derived(data.cards[0]);
19
19
+
let embeddedItem = $derived({
20
20
+
...item,
21
21
+
x: 0,
22
22
+
y: 0,
23
23
+
mobileX: 0,
24
24
+
mobileY: 0
25
25
+
});
26
26
+
27
27
+
let isMobile = $derived((innerWidth.current ?? 1000) < 1024);
28
28
+
setIsMobile(() => isMobile);
29
29
+
30
30
+
const colors = {
31
31
+
base: 'bg-base-200/50 dark:bg-base-950/50',
32
32
+
accent: 'bg-accent-400 dark:bg-accent-500 accent',
33
33
+
transparent: 'bg-transparent'
34
34
+
} as Record<string, string>;
35
35
+
36
36
+
let color = $derived(getColor(item));
37
37
+
let backgroundClass = $derived(color ? (colors[color] ?? colors.accent) : colors.base);
38
38
+
let pageColorClass = $derived(
39
39
+
color !== 'accent' && item?.color !== 'base' && item?.color !== 'transparent' ? color : ''
40
40
+
);
41
41
+
let cardWidth = $derived(Math.max(isMobile ? item.mobileW : item.w, 1));
42
42
+
let cardHeight = $derived(Math.max(isMobile ? item.mobileH : item.h, 1));
43
43
+
44
44
+
let title = $derived.by(() => {
45
45
+
const label = item?.cardData?.label as string | undefined;
46
46
+
const cardName = CardDefinitionsByType[item?.cardType ?? '']?.name;
47
47
+
48
48
+
return label
49
49
+
? `${label} • ${getName(data)}`
50
50
+
: cardName
51
51
+
? `${cardName} • ${getName(data)}`
52
52
+
: getName(data);
53
53
+
});
54
54
+
55
55
+
let description = $derived(
56
56
+
(item?.cardData?.title as string | undefined) ||
57
57
+
(item?.cardData?.text as string | undefined) ||
58
58
+
getDescription(data)
59
59
+
);
60
60
+
61
61
+
const safeJson = (value: string) => JSON.stringify(value).replace(/</g, '\\u003c');
62
62
+
63
63
+
let themeMode = $derived.by(() => {
64
64
+
const theme = page.url.searchParams.get('theme');
65
65
+
return theme === 'dark' || theme === 'light' || theme === 'auto' ? theme : undefined;
66
66
+
});
67
67
+
68
68
+
let themeScript = $derived.by(() => {
69
69
+
if (!themeMode) return '';
70
70
+
71
71
+
return (
72
72
+
`<script>(function(){var theme=${safeJson(themeMode)};var el=document.documentElement;` +
73
73
+
`var apply=function(mode){el.classList.remove('dark','light');` +
74
74
+
`el.classList.add(mode==='auto'&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':mode==='auto'?'light':mode);};` +
75
75
+
`apply(theme);})();<` +
76
76
+
'/script>'
77
77
+
);
78
78
+
});
79
79
+
80
80
+
$effect(() => {
81
81
+
if (!browser || !themeMode) return;
82
82
+
83
83
+
const root = document.documentElement;
84
84
+
const previousHadDark = root.classList.contains('dark');
85
85
+
const previousHadLight = root.classList.contains('light');
86
86
+
87
87
+
const applyTheme = () => {
88
88
+
root.classList.remove('dark', 'light');
89
89
+
90
90
+
if (themeMode === 'auto') {
91
91
+
root.classList.add(
92
92
+
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
93
93
+
);
94
94
+
return;
95
95
+
}
96
96
+
97
97
+
root.classList.add(themeMode);
98
98
+
};
99
99
+
100
100
+
applyTheme();
101
101
+
102
102
+
if (themeMode !== 'auto') {
103
103
+
return () => {
104
104
+
root.classList.remove('dark', 'light');
105
105
+
if (previousHadDark) root.classList.add('dark');
106
106
+
if (previousHadLight) root.classList.add('light');
107
107
+
};
108
108
+
}
109
109
+
110
110
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
111
111
+
mediaQuery.addEventListener('change', applyTheme);
112
112
+
113
113
+
return () => {
114
114
+
mediaQuery.removeEventListener('change', applyTheme);
115
115
+
root.classList.remove('dark', 'light');
116
116
+
if (previousHadDark) root.classList.add('dark');
117
117
+
if (previousHadLight) root.classList.add('light');
118
118
+
};
119
119
+
});
120
120
+
</script>
121
121
+
122
122
+
<Head
123
123
+
favicon={getImage(data.publication, data.did, 'icon') || data.profile.avatar}
124
124
+
{title}
125
125
+
{description}
126
126
+
accentColor={data.publication?.preferences?.accentColor}
127
127
+
baseColor={data.publication?.preferences?.baseColor}
128
128
+
/>
129
129
+
130
130
+
<svelte:head>
131
131
+
<meta name="robots" content="noindex" />
132
132
+
{@html themeScript}
133
133
+
</svelte:head>
134
134
+
135
135
+
<Context {data}>
136
136
+
<QRModalProvider />
137
137
+
<ImageViewerProvider />
138
138
+
139
139
+
<div class={[backgroundClass, pageColorClass, 'embed-page w-full']}>
140
140
+
<div class="embed-stage @container/grid">
141
141
+
<div
142
142
+
class="embed-content"
143
143
+
style={`--embed-ratio: ${cardWidth / cardHeight}; aspect-ratio: ${cardWidth} / ${cardHeight};`}
144
144
+
>
145
145
+
<BaseCard item={embeddedItem} fillPage>
146
146
+
<Card item={embeddedItem} />
147
147
+
</BaseCard>
148
148
+
</div>
149
149
+
</div>
150
150
+
</div>
151
151
+
</Context>
152
152
+
153
153
+
<style>
154
154
+
:global(html),
155
155
+
:global(body) {
156
156
+
min-height: 100%;
157
157
+
}
158
158
+
159
159
+
.embed-page {
160
160
+
min-height: 100vh;
161
161
+
}
162
162
+
163
163
+
.embed-stage {
164
164
+
min-height: 100vh;
165
165
+
display: flex;
166
166
+
align-items: center;
167
167
+
justify-content: center;
168
168
+
padding: clamp(12px, 3vw, 32px);
169
169
+
container-type: inline-size;
170
170
+
}
171
171
+
172
172
+
.embed-content {
173
173
+
width: min(100%, calc((100vh - clamp(24px, 6vw, 64px)) * var(--embed-ratio)));
174
174
+
max-width: calc(100vw - clamp(24px, 6vw, 64px));
175
175
+
max-height: calc(100vh - clamp(24px, 6vw, 64px));
176
176
+
}
177
177
+
</style>
+197
-55
src/lib/website/load.ts
···
1
1
import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto';
2
2
import { CardDefinitionsByType } from '$lib/cards';
3
3
import type { CacheService } from '$lib/cache';
4
4
+
import { createEmptyCard } from '$lib/helper';
4
5
import type { Item, WebsiteData } from '$lib/types';
5
6
import { error } from '@sveltejs/kit';
6
7
import type { ActorIdentifier, Did } from '@atcute/lexicons';
···
85
86
getDetailedProfile({ did })
86
87
]);
87
88
88
88
-
const cardTypes = new Set(cards.map((v) => v.value.cardType ?? '') as string[]);
89
89
-
const cardTypesArray = Array.from(cardTypes);
90
90
-
91
91
-
const additionDataPromises: Record<string, Promise<unknown>> = {};
92
92
-
93
93
-
const loadOptions = { did, handle, cache };
94
94
-
95
95
-
for (const cardType of cardTypesArray) {
96
96
-
const cardDef = CardDefinitionsByType[cardType];
97
97
-
98
98
-
const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[];
99
99
-
100
100
-
try {
101
101
-
if (cardDef?.loadDataServer) {
102
102
-
additionDataPromises[cardType] = cardDef.loadDataServer(items, {
103
103
-
...loadOptions,
104
104
-
env
105
105
-
});
106
106
-
} else if (cardDef?.loadData) {
107
107
-
additionDataPromises[cardType] = cardDef.loadData(items, loadOptions);
108
108
-
}
109
109
-
} catch {
110
110
-
console.error('error getting additional data for', cardType);
111
111
-
}
112
112
-
}
113
113
-
114
114
-
await Promise.all(Object.values(additionDataPromises));
115
115
-
116
116
-
const additionalData: Record<string, unknown> = {};
117
117
-
for (const [key, value] of Object.entries(additionDataPromises)) {
118
118
-
try {
119
119
-
additionalData[key] = await value;
120
120
-
} catch (error) {
121
121
-
console.log('error loading', key, error);
122
122
-
}
123
123
-
}
89
89
+
const additionalData = await loadAdditionalData(
90
90
+
cards.map((v) => ({ ...v.value })) as Item[],
91
91
+
{ did, handle, cache },
92
92
+
env
93
93
+
);
124
94
125
95
const result = {
126
96
page: 'blento.' + page,
···
157
127
return checkData(parsedResult);
158
128
}
159
129
160
160
-
function migrateFromV0ToV1(data: WebsiteData): WebsiteData {
161
161
-
for (const card of data.cards) {
162
162
-
if (card.version) continue;
130
130
+
export async function loadCardData(
131
131
+
handle: ActorIdentifier,
132
132
+
rkey: string,
133
133
+
cache: CacheService | undefined,
134
134
+
env?: Record<string, string | undefined>
135
135
+
): Promise<WebsiteData> {
136
136
+
if (!handle) throw error(404);
137
137
+
if (handle === 'favicon.ico') throw error(404);
138
138
+
139
139
+
let did: Did | undefined = undefined;
140
140
+
if (isHandle(handle)) {
141
141
+
did = await resolveHandle({ handle });
142
142
+
} else if (isDid(handle)) {
143
143
+
did = handle;
144
144
+
} else {
145
145
+
throw error(404);
146
146
+
}
147
147
+
148
148
+
const [cardRecord, profile] = await Promise.all([
149
149
+
getRecord({
150
150
+
did,
151
151
+
collection: 'app.blento.card',
152
152
+
rkey
153
153
+
}).catch(() => undefined),
154
154
+
getDetailedProfile({ did })
155
155
+
]);
156
156
+
157
157
+
if (!cardRecord?.value) {
158
158
+
throw error(404, 'Card not found');
159
159
+
}
160
160
+
161
161
+
const card = migrateCard(structuredClone(cardRecord.value) as Item);
162
162
+
const page = card.page ?? 'blento.self';
163
163
+
164
164
+
const publication = await getRecord({
165
165
+
did,
166
166
+
collection: page === 'blento.self' ? 'site.standard.publication' : 'app.blento.page',
167
167
+
rkey: page
168
168
+
}).catch(() => undefined);
169
169
+
170
170
+
const cards = [card];
171
171
+
const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did);
172
172
+
173
173
+
const additionalData = await loadAdditionalData(
174
174
+
cards,
175
175
+
{ did, handle: resolvedHandle, cache },
176
176
+
env
177
177
+
);
178
178
+
179
179
+
const result = {
180
180
+
page,
181
181
+
handle: resolvedHandle,
182
182
+
did,
183
183
+
cards,
184
184
+
publication:
185
185
+
publication?.value ??
186
186
+
({
187
187
+
name: profile?.displayName || profile?.handle,
188
188
+
description: profile?.description
189
189
+
} as WebsiteData['publication']),
190
190
+
additionalData,
191
191
+
profile,
192
192
+
updatedAt: Date.now(),
193
193
+
version: CURRENT_CACHE_VERSION
194
194
+
};
195
195
+
196
196
+
return result;
197
197
+
}
198
198
+
199
199
+
export async function loadCardTypeData(
200
200
+
handle: ActorIdentifier,
201
201
+
type: string,
202
202
+
cardData: Record<string, unknown>,
203
203
+
cache: CacheService | undefined,
204
204
+
env?: Record<string, string | undefined>
205
205
+
): Promise<WebsiteData> {
206
206
+
if (!handle) throw error(404);
207
207
+
if (handle === 'favicon.ico') throw error(404);
208
208
+
209
209
+
const cardDef = CardDefinitionsByType[type];
210
210
+
if (!cardDef) {
211
211
+
throw error(404, 'Card type not found');
212
212
+
}
213
213
+
214
214
+
let did: Did | undefined = undefined;
215
215
+
if (isHandle(handle)) {
216
216
+
did = await resolveHandle({ handle });
217
217
+
} else if (isDid(handle)) {
218
218
+
did = handle;
219
219
+
} else {
220
220
+
throw error(404);
221
221
+
}
222
222
+
223
223
+
const [publication, profile] = await Promise.all([
224
224
+
getRecord({
225
225
+
did,
226
226
+
collection: 'site.standard.publication',
227
227
+
rkey: 'blento.self'
228
228
+
}).catch(() => undefined),
229
229
+
getDetailedProfile({ did })
230
230
+
]);
231
231
+
232
232
+
const card = createEmptyCard('blento.self');
233
233
+
card.cardType = type;
234
234
+
235
235
+
cardDef.createNew?.(card);
236
236
+
card.cardData = {
237
237
+
...card.cardData,
238
238
+
...cardData
239
239
+
};
240
240
+
241
241
+
const cards = [card];
242
242
+
const resolvedHandle = profile?.handle || (isHandle(handle) ? handle : did);
243
243
+
244
244
+
const additionalData = await loadAdditionalData(
245
245
+
cards,
246
246
+
{ did, handle: resolvedHandle, cache },
247
247
+
env
248
248
+
);
249
249
+
250
250
+
const result = {
251
251
+
page: 'blento.self',
252
252
+
handle: resolvedHandle,
253
253
+
did,
254
254
+
cards,
255
255
+
publication:
256
256
+
publication?.value ??
257
257
+
({
258
258
+
name: profile?.displayName || profile?.handle,
259
259
+
description: profile?.description
260
260
+
} as WebsiteData['publication']),
261
261
+
additionalData,
262
262
+
profile,
263
263
+
updatedAt: Date.now(),
264
264
+
version: CURRENT_CACHE_VERSION
265
265
+
};
266
266
+
267
267
+
return checkData(result);
268
268
+
}
269
269
+
270
270
+
function migrateCard(card: Item): Item {
271
271
+
if (!card.version) {
163
272
card.x *= 2;
164
273
card.y *= 2;
165
274
card.h *= 2;
···
171
280
card.version = 1;
172
281
}
173
282
174
174
-
return data;
283
283
+
if (!card.version || card.version < 2) {
284
284
+
card.page = 'blento.self';
285
285
+
card.version = 2;
286
286
+
}
287
287
+
288
288
+
const cardDef = CardDefinitionsByType[card.cardType];
289
289
+
cardDef?.migrate?.(card);
290
290
+
291
291
+
return card;
175
292
}
176
293
177
177
-
function migrateFromV1ToV2(data: WebsiteData): WebsiteData {
178
178
-
for (const card of data.cards) {
179
179
-
if (!card.version || card.version < 2) {
180
180
-
card.page = 'blento.self';
181
181
-
card.version = 2;
294
294
+
async function loadAdditionalData(
295
295
+
cards: Item[],
296
296
+
{ did, handle, cache }: { did: Did; handle: string; cache?: CacheService },
297
297
+
env?: Record<string, string | undefined>
298
298
+
) {
299
299
+
const cardTypes = new Set(cards.map((v) => v.cardType ?? '') as string[]);
300
300
+
const cardTypesArray = Array.from(cardTypes);
301
301
+
const additionDataPromises: Record<string, Promise<unknown>> = {};
302
302
+
303
303
+
for (const cardType of cardTypesArray) {
304
304
+
const cardDef = CardDefinitionsByType[cardType];
305
305
+
const items = cards.filter((v) => cardType === v.cardType);
306
306
+
307
307
+
try {
308
308
+
if (cardDef?.loadDataServer) {
309
309
+
additionDataPromises[cardType] = cardDef.loadDataServer(items, {
310
310
+
did,
311
311
+
handle,
312
312
+
cache,
313
313
+
env
314
314
+
});
315
315
+
} else if (cardDef?.loadData) {
316
316
+
additionDataPromises[cardType] = cardDef.loadData(items, { did, handle, cache });
317
317
+
}
318
318
+
} catch {
319
319
+
console.error('error getting additional data for', cardType);
182
320
}
183
321
}
184
184
-
return data;
185
185
-
}
186
186
-
187
187
-
function migrateCards(data: WebsiteData): WebsiteData {
188
188
-
for (const card of data.cards) {
189
189
-
const cardDef = CardDefinitionsByType[card.cardType];
190
322
191
191
-
if (!cardDef?.migrate) continue;
323
323
+
await Promise.all(Object.values(additionDataPromises));
192
324
193
193
-
cardDef.migrate(card);
325
325
+
const additionalData: Record<string, unknown> = {};
326
326
+
for (const [key, value] of Object.entries(additionDataPromises)) {
327
327
+
try {
328
328
+
additionalData[key] = await value;
329
329
+
} catch (error) {
330
330
+
console.log('error loading', key, error);
331
331
+
}
194
332
}
195
195
-
return data;
333
333
+
334
334
+
return additionalData;
196
335
}
197
336
198
337
function checkData(data: WebsiteData): WebsiteData {
···
214
353
}
215
354
216
355
function migrateData(data: WebsiteData): WebsiteData {
217
217
-
return migrateCards(migrateFromV1ToV2(migrateFromV0ToV1(data)));
356
356
+
for (const card of data.cards) {
357
357
+
migrateCard(card);
358
358
+
}
359
359
+
return data;
218
360
}
+6
-1
src/routes/+layout.svelte
···
11
11
import LoginModal from '$lib/atproto/UI/LoginModal.svelte';
12
12
13
13
let { children, data } = $props();
14
14
+
let showThemeToggle = $derived(
15
15
+
!/(?:\/card\/[^/]+|\/embed\/type\/[^/]+)$/.test(page.url.pathname)
16
16
+
);
14
17
15
18
const errorMessages: Record<string, (params: URLSearchParams) => string> = {
16
19
handle_not_found: (p) => `Handle ${p.get('handle') ?? ''} not found!`
···
25
28
{@render children()}
26
29
</Tooltip.Provider>
27
30
28
28
-
<ThemeToggle class="fixed top-2 left-2 z-10" />
31
31
+
{#if showThemeToggle}
32
32
+
<ThemeToggle class="fixed top-2 left-2 z-10" />
33
33
+
{/if}
29
34
30
35
<Toaster />
31
36
+16
src/routes/[[actor=actor]]/card/[rkey]/+page.server.ts
···
1
1
+
import { createCache } from '$lib/cache';
2
2
+
import { getActor } from '$lib/actor';
3
3
+
import { loadCardData } from '$lib/website/load';
4
4
+
import { error } from '@sveltejs/kit';
5
5
+
import { env } from '$env/dynamic/private';
6
6
+
7
7
+
export async function load({ params, platform, request }) {
8
8
+
const cache = createCache(platform);
9
9
+
const actor = await getActor({ request, paramActor: params.actor, platform });
10
10
+
11
11
+
if (!actor) {
12
12
+
throw error(404, 'Page not found');
13
13
+
}
14
14
+
15
15
+
return await loadCardData(actor, params.rkey, cache, env);
16
16
+
}
+7
src/routes/[[actor=actor]]/card/[rkey]/+page.svelte
···
1
1
+
<script lang="ts">
2
2
+
import EmbeddedCard from '$lib/website/EmbeddedCard.svelte';
3
3
+
4
4
+
let { data } = $props();
5
5
+
</script>
6
6
+
7
7
+
<EmbeddedCard {data} />
+59
src/routes/[[actor=actor]]/embed/type/[type]/+page.server.ts
···
1
1
+
import { createCache } from '$lib/cache';
2
2
+
import { getActor } from '$lib/actor';
3
3
+
import { loadCardTypeData } from '$lib/website/load';
4
4
+
import { error } from '@sveltejs/kit';
5
5
+
import { env } from '$env/dynamic/private';
6
6
+
7
7
+
function parseQueryParamValue(value: string): unknown {
8
8
+
const trimmed = value.trim();
9
9
+
10
10
+
if (trimmed === 'true') return true;
11
11
+
if (trimmed === 'false') return false;
12
12
+
if (trimmed === 'null') return null;
13
13
+
14
14
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) {
15
15
+
return Number(trimmed);
16
16
+
}
17
17
+
18
18
+
if (
19
19
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
20
20
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
21
21
+
) {
22
22
+
try {
23
23
+
return JSON.parse(trimmed);
24
24
+
} catch {
25
25
+
return value;
26
26
+
}
27
27
+
}
28
28
+
29
29
+
return value;
30
30
+
}
31
31
+
32
32
+
function getCardDataFromSearchParams(searchParams: URLSearchParams) {
33
33
+
const cardData: Record<string, unknown> = {};
34
34
+
const keys = new Set(searchParams.keys());
35
35
+
36
36
+
for (const key of keys) {
37
37
+
const values = searchParams.getAll(key).map(parseQueryParamValue);
38
38
+
cardData[key] = values.length === 1 ? values[0] : values;
39
39
+
}
40
40
+
41
41
+
return cardData;
42
42
+
}
43
43
+
44
44
+
export async function load({ params, platform, request, url }) {
45
45
+
const cache = createCache(platform);
46
46
+
const actor = await getActor({ request, paramActor: params.actor, platform });
47
47
+
48
48
+
if (!actor) {
49
49
+
throw error(404, 'Page not found');
50
50
+
}
51
51
+
52
52
+
return await loadCardTypeData(
53
53
+
actor,
54
54
+
params.type,
55
55
+
getCardDataFromSearchParams(url.searchParams),
56
56
+
cache,
57
57
+
env
58
58
+
);
59
59
+
}
+7
src/routes/[[actor=actor]]/embed/type/[type]/+page.svelte
···
1
1
+
<script lang="ts">
2
2
+
import EmbeddedCard from '$lib/website/EmbeddedCard.svelte';
3
3
+
4
4
+
let { data } = $props();
5
5
+
</script>
6
6
+
7
7
+
<EmbeddedCard {data} />