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
blog creation pt1
Florian
3 weeks ago
23e1a609
ce94823f
+380
-63
6 changed files
expand all
collapse all
unified
split
src
routes
[[actor=actor]]
blog
+page.svelte
new
+page.svelte
events
+page.svelte
[rkey]
+page.server.ts
+page.svelte
EventRsvp.svelte
+11
-2
src/routes/[[actor=actor]]/blog/+page.svelte
···
1
1
<script lang="ts">
2
2
import { getCDNImageBlobUrl } from '$lib/atproto';
3
3
-
import { Avatar as FoxAvatar } from '@foxui/core';
3
3
+
import { user } from '$lib/atproto/auth.svelte';
4
4
+
import { Avatar as FoxAvatar, Button } from '@foxui/core';
4
5
5
6
let { data } = $props();
6
7
···
23
24
return date.toLocaleDateString('en-US', options);
24
25
}
25
26
27
27
+
let actorPrefix = $derived(hostProfile?.handle ? `/${hostProfile.handle}` : `/${did}`);
28
28
+
let isOwner = $derived(user.isLoggedIn && user.did === did);
29
29
+
26
30
function getCoverUrl(
27
31
coverImage: { $type: 'blob'; ref: { $link: string } } | undefined
28
32
): string | undefined {
···
45
49
<div class="mx-auto max-w-4xl">
46
50
<!-- Header -->
47
51
<div class="mb-8">
48
48
-
<h1 class="text-base-900 dark:text-base-50 mb-2 text-2xl font-bold sm:text-3xl">Blog</h1>
52
52
+
<div class="flex items-center justify-between gap-4">
53
53
+
<h1 class="text-base-900 dark:text-base-50 mb-2 text-2xl font-bold sm:text-3xl">Blog</h1>
54
54
+
{#if isOwner}
55
55
+
<Button href="{actorPrefix}/blog/new">New post</Button>
56
56
+
{/if}
57
57
+
</div>
49
58
<div class="mt-4 flex items-center gap-2">
50
59
<span class="text-base-500 dark:text-base-400 text-sm">Written by</span>
51
60
<a
+263
src/routes/[[actor=actor]]/blog/new/+page.svelte
···
1
1
+
<script lang="ts">
2
2
+
import { user } from '$lib/atproto/auth.svelte';
3
3
+
import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte';
4
4
+
import { uploadBlob, createTID } from '$lib/atproto/methods';
5
5
+
import { compressImage } from '$lib/atproto/image-helper';
6
6
+
import { Button } from '@foxui/core';
7
7
+
import { goto } from '$app/navigation';
8
8
+
9
9
+
let title = $state('');
10
10
+
let description = $state('');
11
11
+
let content = $state('');
12
12
+
let coverFile: File | null = $state(null);
13
13
+
let coverPreview: string | null = $state(null);
14
14
+
let submitting = $state(false);
15
15
+
let error: string | null = $state(null);
16
16
+
17
17
+
let fileInput: HTMLInputElement | undefined = $state();
18
18
+
19
19
+
function onFileChange(e: Event) {
20
20
+
const input = e.target as HTMLInputElement;
21
21
+
const file = input.files?.[0];
22
22
+
if (!file) return;
23
23
+
coverFile = file;
24
24
+
if (coverPreview) URL.revokeObjectURL(coverPreview);
25
25
+
coverPreview = URL.createObjectURL(file);
26
26
+
}
27
27
+
28
28
+
function removeCover() {
29
29
+
coverFile = null;
30
30
+
if (coverPreview) {
31
31
+
URL.revokeObjectURL(coverPreview);
32
32
+
coverPreview = null;
33
33
+
}
34
34
+
if (fileInput) fileInput.value = '';
35
35
+
}
36
36
+
37
37
+
async function handleSubmit() {
38
38
+
error = null;
39
39
+
40
40
+
if (!title.trim()) {
41
41
+
error = 'Title is required.';
42
42
+
return;
43
43
+
}
44
44
+
if (!user.client || !user.did) {
45
45
+
error = 'You must be logged in.';
46
46
+
return;
47
47
+
}
48
48
+
49
49
+
submitting = true;
50
50
+
51
51
+
try {
52
52
+
let coverImage: unknown | undefined;
53
53
+
54
54
+
if (coverFile) {
55
55
+
const compressed = await compressImage(coverFile);
56
56
+
const blobRef = await uploadBlob({ blob: compressed.blob });
57
57
+
if (blobRef) {
58
58
+
coverImage = blobRef;
59
59
+
}
60
60
+
}
61
61
+
62
62
+
const rkey = createTID();
63
63
+
64
64
+
const record: Record<string, unknown> = {
65
65
+
$type: 'site.standard.document',
66
66
+
title: title.trim(),
67
67
+
content: { $type: 'app.blento.markdown', value: content },
68
68
+
site: `at://${user.did}/site.standard.publication/blento.self`,
69
69
+
path: `/blog/${rkey}`,
70
70
+
publishedAt: new Date().toISOString()
71
71
+
};
72
72
+
73
73
+
if (description.trim()) {
74
74
+
record.description = description.trim();
75
75
+
}
76
76
+
if (coverImage) {
77
77
+
record.coverImage = coverImage;
78
78
+
}
79
79
+
80
80
+
const response = await user.client.post('com.atproto.repo.createRecord', {
81
81
+
input: {
82
82
+
collection: 'site.standard.document',
83
83
+
repo: user.did,
84
84
+
rkey,
85
85
+
record
86
86
+
}
87
87
+
});
88
88
+
89
89
+
if (response.ok) {
90
90
+
const handle =
91
91
+
user.profile?.handle && user.profile.handle !== 'handle.invalid'
92
92
+
? user.profile.handle
93
93
+
: user.did;
94
94
+
goto(`/${handle}/blog/${rkey}`);
95
95
+
} else {
96
96
+
error = 'Failed to create post. Please try again.';
97
97
+
}
98
98
+
} catch (e) {
99
99
+
console.error('Failed to create post:', e);
100
100
+
error = 'Failed to create post. Please try again.';
101
101
+
} finally {
102
102
+
submitting = false;
103
103
+
}
104
104
+
}
105
105
+
</script>
106
106
+
107
107
+
<svelte:head>
108
108
+
<title>Create Blog Post</title>
109
109
+
</svelte:head>
110
110
+
111
111
+
<div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12">
112
112
+
<div class="mx-auto max-w-2xl">
113
113
+
<h1 class="text-base-900 dark:text-base-50 mb-8 text-3xl font-bold">Create Blog Post</h1>
114
114
+
115
115
+
{#if user.isInitializing}
116
116
+
<div class="flex items-center gap-3">
117
117
+
<div class="bg-base-300 dark:bg-base-700 size-5 animate-pulse rounded-full"></div>
118
118
+
<div class="bg-base-300 dark:bg-base-700 h-4 w-48 animate-pulse rounded"></div>
119
119
+
</div>
120
120
+
{:else if !user.isLoggedIn}
121
121
+
<div
122
122
+
class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-900/50 rounded-2xl border p-8 text-center"
123
123
+
>
124
124
+
<p class="text-base-600 dark:text-base-400 mb-4">Log in to create a blog post.</p>
125
125
+
<Button onclick={() => loginModalState.show()}>Log in</Button>
126
126
+
</div>
127
127
+
{:else}
128
128
+
<form
129
129
+
onsubmit={(e) => {
130
130
+
e.preventDefault();
131
131
+
handleSubmit();
132
132
+
}}
133
133
+
class="space-y-6"
134
134
+
>
135
135
+
<!-- Cover image -->
136
136
+
<div>
137
137
+
<label
138
138
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
139
139
+
for="cover"
140
140
+
>
141
141
+
Cover image
142
142
+
</label>
143
143
+
<input
144
144
+
bind:this={fileInput}
145
145
+
type="file"
146
146
+
id="cover"
147
147
+
accept="image/*"
148
148
+
onchange={onFileChange}
149
149
+
class="hidden"
150
150
+
/>
151
151
+
{#if coverPreview}
152
152
+
<div class="relative inline-block">
153
153
+
<img
154
154
+
src={coverPreview}
155
155
+
alt="Cover preview"
156
156
+
class="border-base-200 dark:border-base-700 h-40 w-40 rounded-xl border object-cover"
157
157
+
/>
158
158
+
<button
159
159
+
type="button"
160
160
+
onclick={removeCover}
161
161
+
aria-label="Remove cover image"
162
162
+
class="bg-base-900/70 absolute -top-2 -right-2 flex size-6 items-center justify-center rounded-full text-white hover:bg-red-600"
163
163
+
>
164
164
+
<svg
165
165
+
xmlns="http://www.w3.org/2000/svg"
166
166
+
viewBox="0 0 20 20"
167
167
+
fill="currentColor"
168
168
+
class="size-3.5"
169
169
+
>
170
170
+
<path
171
171
+
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
172
172
+
/>
173
173
+
</svg>
174
174
+
</button>
175
175
+
</div>
176
176
+
{:else}
177
177
+
<button
178
178
+
type="button"
179
179
+
onclick={() => fileInput?.click()}
180
180
+
class="border-base-300 dark:border-base-700 hover:border-base-400 dark:hover:border-base-600 text-base-500 dark:text-base-400 flex h-40 w-40 flex-col items-center justify-center rounded-xl border-2 border-dashed transition-colors"
181
181
+
>
182
182
+
<svg
183
183
+
xmlns="http://www.w3.org/2000/svg"
184
184
+
fill="none"
185
185
+
viewBox="0 0 24 24"
186
186
+
stroke-width="1.5"
187
187
+
stroke="currentColor"
188
188
+
class="mb-1 size-6"
189
189
+
>
190
190
+
<path
191
191
+
stroke-linecap="round"
192
192
+
stroke-linejoin="round"
193
193
+
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0 0 22.5 18.75V5.25A2.25 2.25 0 0 0 20.25 3H3.75A2.25 2.25 0 0 0 1.5 5.25v13.5A2.25 2.25 0 0 0 3.75 21Z"
194
194
+
/>
195
195
+
</svg>
196
196
+
<span class="text-xs">Upload image</span>
197
197
+
</button>
198
198
+
{/if}
199
199
+
</div>
200
200
+
201
201
+
<!-- Title -->
202
202
+
<div>
203
203
+
<label
204
204
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
205
205
+
for="title"
206
206
+
>
207
207
+
Title <span class="text-red-500">*</span>
208
208
+
</label>
209
209
+
<input
210
210
+
type="text"
211
211
+
id="title"
212
212
+
bind:value={title}
213
213
+
required
214
214
+
placeholder="Post title"
215
215
+
class="border-base-300 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 placeholder:text-base-400 dark:placeholder:text-base-600 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
216
216
+
/>
217
217
+
</div>
218
218
+
219
219
+
<!-- Description -->
220
220
+
<div>
221
221
+
<label
222
222
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
223
223
+
for="description"
224
224
+
>
225
225
+
Description
226
226
+
</label>
227
227
+
<textarea
228
228
+
id="description"
229
229
+
bind:value={description}
230
230
+
rows={2}
231
231
+
placeholder="A short summary of the post"
232
232
+
class="border-base-300 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 placeholder:text-base-400 dark:placeholder:text-base-600 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
233
233
+
></textarea>
234
234
+
</div>
235
235
+
236
236
+
<!-- Content -->
237
237
+
<div>
238
238
+
<label
239
239
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
240
240
+
for="content"
241
241
+
>
242
242
+
Content
243
243
+
</label>
244
244
+
<textarea
245
245
+
id="content"
246
246
+
bind:value={content}
247
247
+
rows={12}
248
248
+
placeholder="Write your post content in markdown..."
249
249
+
class="border-base-300 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 placeholder:text-base-400 dark:placeholder:text-base-600 w-full rounded-lg border px-3 py-2 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
250
250
+
></textarea>
251
251
+
</div>
252
252
+
253
253
+
{#if error}
254
254
+
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
255
255
+
{/if}
256
256
+
257
257
+
<Button type="submit" disabled={submitting} class="w-full">
258
258
+
{submitting ? 'Publishing...' : 'Publish Post'}
259
259
+
</Button>
260
260
+
</form>
261
261
+
{/if}
262
262
+
</div>
263
263
+
</div>
+45
-36
src/routes/[[actor=actor]]/events/+page.svelte
···
1
1
<script lang="ts">
2
2
import type { EventData } from '$lib/cards/social/EventCard';
3
3
import { getCDNImageBlobUrl } from '$lib/atproto';
4
4
-
import { Avatar as FoxAvatar, Badge } from '@foxui/core';
4
4
+
import { user } from '$lib/atproto/auth.svelte';
5
5
+
import { Avatar as FoxAvatar, Badge, Button } from '@foxui/core';
5
6
import Avatar from 'svelte-boring-avatars';
6
7
7
8
let { data } = $props();
···
75
76
}
76
77
77
78
let actorPrefix = $derived(data.hostProfile?.handle ? `/${data.hostProfile.handle}` : `/${did}`);
79
79
+
let isOwner = $derived(user.isLoggedIn && user.did === did);
78
80
</script>
79
81
80
82
<svelte:head>
···
90
92
<div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12 sm:py-12">
91
93
<div class="mx-auto max-w-4xl">
92
94
<!-- Header -->
93
93
-
<div class="mb-8">
94
94
-
<h1 class="text-base-900 dark:text-base-50 mb-2 text-2xl font-bold sm:text-3xl">
95
95
-
Upcoming events
96
96
-
</h1>
97
97
-
<div class="mt-4 flex items-center gap-2">
98
98
-
<span class="text-base-500 dark:text-base-400 text-sm">Hosted by</span>
99
99
-
<a
100
100
-
href={hostUrl}
101
101
-
target={hostProfile?.hasBlento ? undefined : '_blank'}
102
102
-
rel={hostProfile?.hasBlento ? undefined : 'noopener noreferrer'}
103
103
-
class="flex items-center gap-1.5 hover:underline"
104
104
-
>
105
105
-
<FoxAvatar src={hostProfile?.avatar} alt={hostName} class="size-5 shrink-0" />
106
106
-
<span class="text-base-900 dark:text-base-100 text-sm font-medium">{hostName}</span>
107
107
-
</a>
95
95
+
<div class="mb-8 flex items-start justify-between">
96
96
+
<div>
97
97
+
<h1 class="text-base-900 dark:text-base-50 mb-2 text-2xl font-bold sm:text-3xl">
98
98
+
Upcoming events
99
99
+
</h1>
100
100
+
<div class="mt-4 flex items-center gap-2">
101
101
+
<span class="text-base-500 dark:text-base-400 text-sm">Hosted by</span>
102
102
+
<a
103
103
+
href={hostUrl}
104
104
+
target={hostProfile?.hasBlento ? undefined : '_blank'}
105
105
+
rel={hostProfile?.hasBlento ? undefined : 'noopener noreferrer'}
106
106
+
class="flex items-center gap-1.5 hover:underline"
107
107
+
>
108
108
+
<FoxAvatar src={hostProfile?.avatar} alt={hostName} class="size-5 shrink-0" />
109
109
+
<span class="text-base-900 dark:text-base-100 text-sm font-medium">{hostName}</span>
110
110
+
</a>
111
111
+
</div>
108
112
</div>
113
113
+
{#if isOwner}
114
114
+
<Button href="{actorPrefix}/events/new" variant="primary">New event</Button>
115
115
+
{/if}
109
116
</div>
110
117
111
118
{#if events.length === 0}
···
117
124
{@const location = getLocationString(event.locations)}
118
125
{@const rkey = event.rkey}
119
126
<a
120
120
-
href="{actorPrefix}/e/{rkey}"
121
121
-
class="border-base-200 dark:border-base-800 hover:border-base-300 dark:hover:border-base-700 group block overflow-hidden rounded-xl border transition-colors"
127
127
+
href="{actorPrefix}/events/{rkey}"
128
128
+
class="border-base-200 dark:border-base-800 hover:border-base-300 dark:hover:border-base-700 group block overflow-hidden rounded-2xl border transition-colors"
122
129
>
123
130
<!-- Thumbnail -->
124
124
-
{#if thumbnail}
125
125
-
<img
126
126
-
src={thumbnail.url}
127
127
-
alt={thumbnail.alt}
128
128
-
class="aspect-square w-full object-cover"
129
129
-
/>
130
130
-
{:else}
131
131
-
<div
132
132
-
class="bg-base-100 dark:bg-base-900 aspect-square w-full [&>svg]:h-full [&>svg]:w-full"
133
133
-
>
134
134
-
<Avatar
135
135
-
size={400}
136
136
-
name={rkey}
137
137
-
variant="marble"
138
138
-
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
139
139
-
square
131
131
+
<div class="p-4">
132
132
+
{#if thumbnail}
133
133
+
<img
134
134
+
src={thumbnail.url}
135
135
+
alt={thumbnail.alt}
136
136
+
class="aspect-square w-full rounded-2xl object-cover"
140
137
/>
141
141
-
</div>
142
142
-
{/if}
138
138
+
{:else}
139
139
+
<div
140
140
+
class="bg-base-100 dark:bg-base-900 aspect-square w-full overflow-hidden rounded-2xl [&>svg]:h-full [&>svg]:w-full"
141
141
+
>
142
142
+
<Avatar
143
143
+
size={400}
144
144
+
name={rkey}
145
145
+
variant="marble"
146
146
+
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
147
147
+
square
148
148
+
/>
149
149
+
</div>
150
150
+
{/if}
151
151
+
</div>
143
152
144
153
<!-- Content -->
145
154
<div class="p-4">
+1
src/routes/[[actor=actor]]/events/[rkey]/+page.server.ts
···
44
44
}
45
45
46
46
const eventData: EventData = eventRecord.value as EventData;
47
47
+
console.log(eventData);
47
48
48
49
return {
49
50
eventData,
+59
-24
src/routes/[[actor=actor]]/events/[rkey]/+page.svelte
···
79
79
80
80
let location = $derived(getLocationString(eventData.locations));
81
81
82
82
-
let headerImage = $derived.by(() => {
82
82
+
let thumbnailImage = $derived.by(() => {
83
83
if (!eventData.media || eventData.media.length === 0) return null;
84
84
const media = eventData.media.find((m) => m.role === 'thumbnail');
85
85
if (!media?.content) return null;
···
88
88
return { url, alt: media.alt || eventData.name };
89
89
});
90
90
91
91
+
let bannerImage = $derived.by(() => {
92
92
+
if (!eventData.media || eventData.media.length === 0) return null;
93
93
+
const media = eventData.media.find((m) => m.role === 'header');
94
94
+
if (!media?.content) return null;
95
95
+
const url = getCDNImageBlobUrl({ did, blob: media.content, type: 'jpeg' });
96
96
+
if (!url) return null;
97
97
+
return { url, alt: media.alt || eventData.name };
98
98
+
});
99
99
+
100
100
+
// Prefer thumbnail; fall back to header/banner image
101
101
+
let displayImage = $derived(thumbnailImage ?? bannerImage);
102
102
+
let isBannerOnly = $derived(!thumbnailImage && !!bannerImage);
103
103
+
104
104
+
let isSameDay = $derived(
105
105
+
endDate &&
106
106
+
startDate.getFullYear() === endDate.getFullYear() &&
107
107
+
startDate.getMonth() === endDate.getMonth() &&
108
108
+
startDate.getDate() === endDate.getDate()
109
109
+
);
110
110
+
91
111
let smokesignalUrl = $derived(`https://smokesignal.events/${did}/${rkey}`);
92
112
let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`);
93
113
···
108
128
109
129
<div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12 sm:py-12">
110
130
<div class="mx-auto max-w-4xl">
131
131
+
<!-- Banner image (full width, only when no thumbnail) -->
132
132
+
{#if isBannerOnly && displayImage}
133
133
+
<img
134
134
+
src={displayImage.url}
135
135
+
alt={displayImage.alt}
136
136
+
class="border-base-200 dark:border-base-800 mb-8 aspect-[3/1] w-full rounded-2xl border object-cover"
137
137
+
/>
138
138
+
{/if}
139
139
+
111
140
<!-- Two-column layout: image left, details right -->
112
141
<div
113
142
class="grid grid-cols-1 gap-8 md:grid-cols-[14rem_1fr] md:gap-x-10 md:gap-y-6 lg:grid-cols-[16rem_1fr]"
114
143
>
115
115
-
<!-- Image -->
116
116
-
<div class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none">
117
117
-
{#if headerImage}
118
118
-
<img
119
119
-
src={headerImage.url}
120
120
-
alt={headerImage.alt}
121
121
-
class="border-base-200 dark:border-base-800 aspect-square w-full rounded-2xl border object-cover"
122
122
-
/>
123
123
-
{:else}
124
124
-
<div
125
125
-
class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border [&>svg]:h-full [&>svg]:w-full"
126
126
-
>
127
127
-
<Avatar
128
128
-
size={256}
129
129
-
name={data.rkey}
130
130
-
variant="marble"
131
131
-
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
132
132
-
square
144
144
+
<!-- Thumbnail image (left column) -->
145
145
+
{#if !isBannerOnly}
146
146
+
<div class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none">
147
147
+
{#if displayImage}
148
148
+
<img
149
149
+
src={displayImage.url}
150
150
+
alt={displayImage.alt}
151
151
+
class="border-base-200 dark:border-base-800 aspect-square w-full rounded-2xl border object-cover"
133
152
/>
134
134
-
</div>
135
135
-
{/if}
136
136
-
</div>
153
153
+
{:else}
154
154
+
<div
155
155
+
class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border [&>svg]:h-full [&>svg]:w-full"
156
156
+
>
157
157
+
<Avatar
158
158
+
size={256}
159
159
+
name={data.rkey}
160
160
+
variant="marble"
161
161
+
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
162
162
+
square
163
163
+
/>
164
164
+
</div>
165
165
+
{/if}
166
166
+
</div>
167
167
+
{/if}
137
168
138
169
<!-- Right column: event details -->
139
170
<div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-5 md:row-start-1">
···
167
198
<div>
168
199
<p class="text-base-900 dark:text-base-50 font-semibold">
169
200
{formatWeekday(startDate)}, {formatFullDate(startDate)}
201
201
+
{#if endDate && !isSameDay}
202
202
+
- {formatWeekday(endDate)}, {formatFullDate(endDate)}
203
203
+
{/if}
170
204
</p>
171
205
<p class="text-base-500 dark:text-base-400 text-sm">
172
206
{formatTime(startDate)}
173
173
-
{#if endDate}
174
174
-
- {formatTime(endDate)}{/if}
207
207
+
{#if endDate && isSameDay}
208
208
+
- {formatTime(endDate)}
209
209
+
{/if}
175
210
</p>
176
211
</div>
177
212
</div>
+1
-1
src/routes/[[actor=actor]]/events/[rkey]/EventRsvp.svelte
···
120
120
</script>
121
121
122
122
<div
123
123
-
class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-900/50 mt-8 mb-2 rounded-2xl border p-4"
123
123
+
class="border-base-200 dark:border-base-800 bg-base-100 items-between dark:bg-base-900/50 mt-8 mb-2 flex h-25 flex-col justify-center rounded-2xl border p-4"
124
124
>
125
125
{#if user.isInitializing || rsvpLoading}
126
126
<div class="flex items-center gap-3">