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
add event creation page
Florian
3 weeks ago
ce94823f
1cc74545
+295
1 changed file
expand all
collapse all
unified
split
src
routes
[[actor=actor]]
events
new
+page.svelte
+295
src/routes/[[actor=actor]]/events/new/+page.svelte
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<script lang="ts">
2
+
import { user } from '$lib/atproto/auth.svelte';
3
+
import { loginModalState } from '$lib/atproto/UI/LoginModal.svelte';
4
+
import { uploadBlob } from '$lib/atproto/methods';
5
+
import { compressImage } from '$lib/atproto/image-helper';
6
+
import { Button } from '@foxui/core';
7
+
import { goto } from '$app/navigation';
8
+
9
+
let name = $state('');
10
+
let description = $state('');
11
+
let startsAt = $state('');
12
+
let endsAt = $state('');
13
+
let thumbnailFile: File | null = $state(null);
14
+
let thumbnailPreview: string | null = $state(null);
15
+
let submitting = $state(false);
16
+
let error: string | null = $state(null);
17
+
18
+
let fileInput: HTMLInputElement | undefined = $state();
19
+
20
+
function onFileChange(e: Event) {
21
+
const input = e.target as HTMLInputElement;
22
+
const file = input.files?.[0];
23
+
if (!file) return;
24
+
thumbnailFile = file;
25
+
if (thumbnailPreview) URL.revokeObjectURL(thumbnailPreview);
26
+
thumbnailPreview = URL.createObjectURL(file);
27
+
}
28
+
29
+
function removeThumbnail() {
30
+
thumbnailFile = null;
31
+
if (thumbnailPreview) {
32
+
URL.revokeObjectURL(thumbnailPreview);
33
+
thumbnailPreview = null;
34
+
}
35
+
if (fileInput) fileInput.value = '';
36
+
}
37
+
38
+
async function handleSubmit() {
39
+
error = null;
40
+
41
+
if (!name.trim()) {
42
+
error = 'Name is required.';
43
+
return;
44
+
}
45
+
if (!startsAt) {
46
+
error = 'Start date is required.';
47
+
return;
48
+
}
49
+
if (!user.client || !user.did) {
50
+
error = 'You must be logged in.';
51
+
return;
52
+
}
53
+
54
+
submitting = true;
55
+
56
+
try {
57
+
let media: Array<Record<string, unknown>> | undefined;
58
+
59
+
if (thumbnailFile) {
60
+
const compressed = await compressImage(thumbnailFile);
61
+
const blobRef = await uploadBlob({ blob: compressed.blob });
62
+
if (blobRef) {
63
+
media = [
64
+
{
65
+
role: 'thumbnail',
66
+
content: blobRef,
67
+
aspect_ratio: {
68
+
width: compressed.aspectRatio.width,
69
+
height: compressed.aspectRatio.height
70
+
}
71
+
}
72
+
];
73
+
}
74
+
}
75
+
76
+
const record: Record<string, unknown> = {
77
+
$type: 'community.lexicon.calendar.event',
78
+
name: name.trim(),
79
+
mode: 'community.lexicon.calendar.event#inperson',
80
+
status: 'community.lexicon.calendar.event#scheduled',
81
+
startsAt: new Date(startsAt).toISOString(),
82
+
createdAt: new Date().toISOString()
83
+
};
84
+
85
+
if (description.trim()) {
86
+
record.description = description.trim();
87
+
}
88
+
if (endsAt) {
89
+
record.endsAt = new Date(endsAt).toISOString();
90
+
}
91
+
if (media) {
92
+
record.media = media;
93
+
}
94
+
95
+
const response = await user.client.post('com.atproto.repo.createRecord', {
96
+
input: {
97
+
collection: 'community.lexicon.calendar.event',
98
+
repo: user.did,
99
+
record
100
+
}
101
+
});
102
+
103
+
if (response.ok) {
104
+
const parts = response.data.uri.split('/');
105
+
const rkey = parts[parts.length - 1];
106
+
const handle =
107
+
user.profile?.handle && user.profile.handle !== 'handle.invalid'
108
+
? user.profile.handle
109
+
: user.did;
110
+
goto(`/${handle}/e/${rkey}`);
111
+
} else {
112
+
error = 'Failed to create event. Please try again.';
113
+
}
114
+
} catch (e) {
115
+
console.error('Failed to create event:', e);
116
+
error = 'Failed to create event. Please try again.';
117
+
} finally {
118
+
submitting = false;
119
+
}
120
+
}
121
+
</script>
122
+
123
+
<svelte:head>
124
+
<title>Create Event</title>
125
+
</svelte:head>
126
+
127
+
<div class="bg-base-50 dark:bg-base-950 min-h-screen px-6 py-12">
128
+
<div class="mx-auto max-w-2xl">
129
+
<h1 class="text-base-900 dark:text-base-50 mb-8 text-3xl font-bold">Create Event</h1>
130
+
131
+
{#if user.isInitializing}
132
+
<div class="flex items-center gap-3">
133
+
<div class="bg-base-300 dark:bg-base-700 size-5 animate-pulse rounded-full"></div>
134
+
<div class="bg-base-300 dark:bg-base-700 h-4 w-48 animate-pulse rounded"></div>
135
+
</div>
136
+
{:else if !user.isLoggedIn}
137
+
<div
138
+
class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-900/50 rounded-2xl border p-8 text-center"
139
+
>
140
+
<p class="text-base-600 dark:text-base-400 mb-4">Log in to create an event.</p>
141
+
<Button onclick={() => loginModalState.show()}>Log in</Button>
142
+
</div>
143
+
{:else}
144
+
<form
145
+
onsubmit={(e) => {
146
+
e.preventDefault();
147
+
handleSubmit();
148
+
}}
149
+
class="space-y-6"
150
+
>
151
+
<!-- Thumbnail -->
152
+
<div>
153
+
<label
154
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
155
+
for="thumbnail"
156
+
>
157
+
Thumbnail
158
+
</label>
159
+
<input
160
+
bind:this={fileInput}
161
+
type="file"
162
+
id="thumbnail"
163
+
accept="image/*"
164
+
onchange={onFileChange}
165
+
class="hidden"
166
+
/>
167
+
{#if thumbnailPreview}
168
+
<div class="relative inline-block">
169
+
<img
170
+
src={thumbnailPreview}
171
+
alt="Thumbnail preview"
172
+
class="border-base-200 dark:border-base-700 h-40 w-40 rounded-xl border object-cover"
173
+
/>
174
+
<button
175
+
type="button"
176
+
onclick={removeThumbnail}
177
+
aria-label="Remove thumbnail"
178
+
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"
179
+
>
180
+
<svg
181
+
xmlns="http://www.w3.org/2000/svg"
182
+
viewBox="0 0 20 20"
183
+
fill="currentColor"
184
+
class="size-3.5"
185
+
>
186
+
<path
187
+
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"
188
+
/>
189
+
</svg>
190
+
</button>
191
+
</div>
192
+
{:else}
193
+
<button
194
+
type="button"
195
+
onclick={() => fileInput?.click()}
196
+
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"
197
+
>
198
+
<svg
199
+
xmlns="http://www.w3.org/2000/svg"
200
+
fill="none"
201
+
viewBox="0 0 24 24"
202
+
stroke-width="1.5"
203
+
stroke="currentColor"
204
+
class="mb-1 size-6"
205
+
>
206
+
<path
207
+
stroke-linecap="round"
208
+
stroke-linejoin="round"
209
+
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"
210
+
/>
211
+
</svg>
212
+
<span class="text-xs">Upload image</span>
213
+
</button>
214
+
{/if}
215
+
</div>
216
+
217
+
<!-- Name -->
218
+
<div>
219
+
<label
220
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
221
+
for="name"
222
+
>
223
+
Name <span class="text-red-500">*</span>
224
+
</label>
225
+
<input
226
+
type="text"
227
+
id="name"
228
+
bind:value={name}
229
+
required
230
+
placeholder="Event name"
231
+
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"
232
+
/>
233
+
</div>
234
+
235
+
<!-- Description -->
236
+
<div>
237
+
<label
238
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
239
+
for="description"
240
+
>
241
+
Description
242
+
</label>
243
+
<textarea
244
+
id="description"
245
+
bind:value={description}
246
+
rows={4}
247
+
placeholder="What's this event about?"
248
+
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"
249
+
></textarea>
250
+
</div>
251
+
252
+
<!-- Start date/time -->
253
+
<div>
254
+
<label
255
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
256
+
for="startsAt"
257
+
>
258
+
Start date & time <span class="text-red-500">*</span>
259
+
</label>
260
+
<input
261
+
type="datetime-local"
262
+
id="startsAt"
263
+
bind:value={startsAt}
264
+
required
265
+
class="border-base-300 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
266
+
/>
267
+
</div>
268
+
269
+
<!-- End date/time -->
270
+
<div>
271
+
<label
272
+
class="text-base-700 dark:text-base-300 mb-1.5 block text-sm font-medium"
273
+
for="endsAt"
274
+
>
275
+
End date & time
276
+
</label>
277
+
<input
278
+
type="datetime-local"
279
+
id="endsAt"
280
+
bind:value={endsAt}
281
+
class="border-base-300 dark:border-base-700 bg-base-50 dark:bg-base-900 text-base-900 dark:text-base-100 w-full rounded-lg border px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
282
+
/>
283
+
</div>
284
+
285
+
{#if error}
286
+
<p class="text-sm text-red-600 dark:text-red-400">{error}</p>
287
+
{/if}
288
+
289
+
<Button type="submit" disabled={submitting} class="w-full">
290
+
{submitting ? 'Creating...' : 'Create Event'}
291
+
</Button>
292
+
</form>
293
+
{/if}
294
+
</div>
295
+
</div>