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