@atmo-dev/kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -0
- package/dist/at-tags.d.ts +18 -0
- package/dist/at-tags.js +18 -0
- package/dist/components/AtTags.svelte +17 -0
- package/dist/components/AtTags.svelte.d.ts +7 -0
- package/dist/events/DatePicker.svelte +222 -0
- package/dist/events/DatePicker.svelte.d.ts +12 -0
- package/dist/events/DateTimePicker.svelte +112 -0
- package/dist/events/DateTimePicker.svelte.d.ts +10 -0
- package/dist/events/EventAttendees.svelte +199 -0
- package/dist/events/EventAttendees.svelte.d.ts +19 -0
- package/dist/events/EventCard.svelte +148 -0
- package/dist/events/EventCard.svelte.d.ts +8 -0
- package/dist/events/EventComments.svelte +98 -0
- package/dist/events/EventComments.svelte.d.ts +8 -0
- package/dist/events/EventEditor.svelte +540 -0
- package/dist/events/EventEditor.svelte.d.ts +18 -0
- package/dist/events/EventRsvp.svelte +178 -0
- package/dist/events/EventRsvp.svelte.d.ts +16 -0
- package/dist/events/EventView.svelte +488 -0
- package/dist/events/EventView.svelte.d.ts +23 -0
- package/dist/events/ImageDropper.svelte +66 -0
- package/dist/events/ImageDropper.svelte.d.ts +7 -0
- package/dist/events/Map.svelte +36 -0
- package/dist/events/Map.svelte.d.ts +8 -0
- package/dist/events/PostToBlueskyModal.svelte +246 -0
- package/dist/events/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/events/ShareModal.svelte +174 -0
- package/dist/events/ShareModal.svelte.d.ts +23 -0
- package/dist/events/StarterPackButton.svelte +72 -0
- package/dist/events/StarterPackButton.svelte.d.ts +10 -0
- package/dist/events/ThemeApply.svelte +30 -0
- package/dist/events/ThemeApply.svelte.d.ts +7 -0
- package/dist/events/ThemeBackground.svelte +33 -0
- package/dist/events/ThemeBackground.svelte.d.ts +7 -0
- package/dist/events/ThemePicker.svelte +104 -0
- package/dist/events/ThemePicker.svelte.d.ts +7 -0
- package/dist/events/ThumbnailPresets.svelte +71 -0
- package/dist/events/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/events/TimePicker.svelte +117 -0
- package/dist/events/TimePicker.svelte.d.ts +10 -0
- package/dist/events/TimezonePicker.svelte +67 -0
- package/dist/events/TimezonePicker.svelte.d.ts +6 -0
- package/dist/events/VodPlayer.svelte +154 -0
- package/dist/events/VodPlayer.svelte.d.ts +14 -0
- package/dist/events/VodTranscript.svelte +75 -0
- package/dist/events/VodTranscript.svelte.d.ts +8 -0
- package/dist/events/atproto-helpers.d.ts +21 -0
- package/dist/events/atproto-helpers.js +61 -0
- package/dist/events/cal/helper.d.ts +1 -0
- package/dist/events/cal/helper.js +20 -0
- package/dist/events/cal/ical.d.ts +22 -0
- package/dist/events/cal/ical.js +188 -0
- package/dist/events/cal/sanitize.d.ts +3 -0
- package/dist/events/cal/sanitize.js +25 -0
- package/dist/events/conference.d.ts +65 -0
- package/dist/events/conference.js +61 -0
- package/dist/events/contrail.d.ts +49 -0
- package/dist/events/contrail.js +12 -0
- package/dist/events/date-format.d.ts +22 -0
- package/dist/events/date-format.js +43 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte +104 -0
- package/dist/events/editor/LinkedRecordsPicker.svelte.d.ts +12 -0
- package/dist/events/editor/LinksSection.svelte +154 -0
- package/dist/events/editor/LinksSection.svelte.d.ts +13 -0
- package/dist/events/editor/LocationSection.svelte +163 -0
- package/dist/events/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/events/editor/RecurringModal.svelte +313 -0
- package/dist/events/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/events/editor/ThemeSection.svelte +24 -0
- package/dist/events/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/events/editor/ThumbnailSection.svelte +181 -0
- package/dist/events/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/events/editor/adapter.d.ts +73 -0
- package/dist/events/editor/adapter.js +9 -0
- package/dist/events/editor/location.d.ts +16 -0
- package/dist/events/editor/location.js +50 -0
- package/dist/events/editor/save.d.ts +43 -0
- package/dist/events/editor/save.js +168 -0
- package/dist/events/editor/types.d.ts +39 -0
- package/dist/events/editor/types.js +9 -0
- package/dist/events/event-media.d.ts +4 -0
- package/dist/events/event-media.js +4 -0
- package/dist/events/event-types.d.ts +79 -0
- package/dist/events/event-types.js +11 -0
- package/dist/events/event-view/AddToCalendarButton.svelte +31 -0
- package/dist/events/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/events/event-view/EventBadges.svelte +20 -0
- package/dist/events/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/events/event-view/EventDateBlock.svelte +43 -0
- package/dist/events/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventHostedBy.svelte +61 -0
- package/dist/events/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/events/event-view/EventLinksList.svelte +24 -0
- package/dist/events/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/events/event-view/EventLocationBlock.svelte +62 -0
- package/dist/events/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/events/event-view/EventLocationMap.svelte +66 -0
- package/dist/events/event-view/EventLocationMap.svelte.d.ts +7 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte +34 -0
- package/dist/events/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte +209 -0
- package/dist/events/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/events/event-view/format.d.ts +10 -0
- package/dist/events/event-view/format.js +112 -0
- package/dist/events/event-view/locations.d.ts +36 -0
- package/dist/events/event-view/locations.js +243 -0
- package/dist/events/event-view/plyr-icons.d.ts +3 -0
- package/dist/events/event-view/plyr-icons.js +46 -0
- package/dist/events/linked-records.d.ts +18 -0
- package/dist/events/linked-records.js +11 -0
- package/dist/events/profile-url.d.ts +3 -0
- package/dist/events/profile-url.js +11 -0
- package/dist/events/schedule/ConferenceTimetable.svelte +340 -0
- package/dist/events/schedule/ConferenceTimetable.svelte.d.ts +22 -0
- package/dist/events/schedule/DaySchedule.svelte +298 -0
- package/dist/events/schedule/DaySchedule.svelte.d.ts +26 -0
- package/dist/events/schedule/ScheduleEventCell.svelte +166 -0
- package/dist/events/schedule/ScheduleEventCell.svelte.d.ts +25 -0
- package/dist/events/schedule/schedule-utils.d.ts +44 -0
- package/dist/events/schedule/schedule-utils.js +166 -0
- package/dist/events/starter-pack-url.d.ts +3 -0
- package/dist/events/starter-pack-url.js +16 -0
- package/dist/events/theme.d.ts +9 -0
- package/dist/events/theme.js +36 -0
- package/dist/events/themes/Blobs.svelte +64 -0
- package/dist/events/themes/Blobs.svelte.d.ts +18 -0
- package/dist/events/themes/Butterflies.svelte +183 -0
- package/dist/events/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/events/themes/Fireflies.svelte +134 -0
- package/dist/events/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/events/themes/Kaleidoscope.svelte +183 -0
- package/dist/events/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/events/themes/Matrix.svelte +153 -0
- package/dist/events/themes/Matrix.svelte.d.ts +3 -0
- package/dist/events/themes/Stars.svelte +98 -0
- package/dist/events/themes/Stars.svelte.d.ts +3 -0
- package/dist/events/thumbnails/designs.d.ts +18 -0
- package/dist/events/thumbnails/designs.js +316 -0
- package/dist/events/view-data.d.ts +38 -0
- package/dist/events/view-data.js +1 -0
- package/dist/events-data.d.ts +15 -0
- package/dist/events-data.js +9 -0
- package/dist/events-editor.d.ts +2 -0
- package/dist/events-editor.js +2 -0
- package/dist/events.d.ts +19 -0
- package/dist/events.js +17 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +50 -4
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getCDNImageBlobUrl } from './atproto-helpers.js';
|
|
3
|
+
import { getEventThumbnail } from './event-media.js';
|
|
4
|
+
import { Avatar, Button, ToggleGroup, ToggleGroupItem } from '../index.js';
|
|
5
|
+
import { onMount } from 'svelte';
|
|
6
|
+
import { InlineEditor, type Editor } from '../index.js';
|
|
7
|
+
import DateTimePicker from './DateTimePicker.svelte';
|
|
8
|
+
import TimezonePicker from './TimezonePicker.svelte';
|
|
9
|
+
import { parseDateTime } from '@internationalized/date';
|
|
10
|
+
import { isoToDatetimeLocalInTz } from './date-format.js';
|
|
11
|
+
import type { FlatEventRecord } from './contrail.js';
|
|
12
|
+
import ThemeApply from './ThemeApply.svelte';
|
|
13
|
+
import ThemeBackground from './ThemeBackground.svelte';
|
|
14
|
+
import { defaultTheme, type EventTheme } from './theme.js';
|
|
15
|
+
|
|
16
|
+
import ThumbnailSection from './editor/ThumbnailSection.svelte';
|
|
17
|
+
import LocationSection from './editor/LocationSection.svelte';
|
|
18
|
+
import LinksSection from './editor/LinksSection.svelte';
|
|
19
|
+
import ThemeSection from './editor/ThemeSection.svelte';
|
|
20
|
+
import RecurringModal from './editor/RecurringModal.svelte';
|
|
21
|
+
import {
|
|
22
|
+
stripModePrefix,
|
|
23
|
+
type EventEditorPrefill,
|
|
24
|
+
type EventLocation,
|
|
25
|
+
type EventMode,
|
|
26
|
+
type Visibility
|
|
27
|
+
} from './editor/types';
|
|
28
|
+
import { buildEventRecord, buildThumbnailMedia, renderPresetThumbnail } from './editor/save';
|
|
29
|
+
import { LocationValidationError } from './editor/location';
|
|
30
|
+
import { DEFAULT_PRESET, hashSeed } from './thumbnails/designs';
|
|
31
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
eventData = null,
|
|
35
|
+
actorDid,
|
|
36
|
+
rkey,
|
|
37
|
+
adapter,
|
|
38
|
+
viewer,
|
|
39
|
+
initialTheme,
|
|
40
|
+
prefill = null
|
|
41
|
+
}: {
|
|
42
|
+
eventData: FlatEventRecord | null;
|
|
43
|
+
actorDid: string;
|
|
44
|
+
rkey: string;
|
|
45
|
+
adapter: EditorAdapter;
|
|
46
|
+
viewer: EditorViewer;
|
|
47
|
+
/** Override default theme for new events (e.g. inherit embedder's palette). */
|
|
48
|
+
initialTheme?: Partial<EventTheme>;
|
|
49
|
+
/** Autofill payload for new events (e.g. imported from Luma/Meetup). */
|
|
50
|
+
prefill?: EventEditorPrefill | null;
|
|
51
|
+
} = $props();
|
|
52
|
+
|
|
53
|
+
let isNew = $derived(eventData === null);
|
|
54
|
+
|
|
55
|
+
// svelte-ignore state_referenced_locally
|
|
56
|
+
let thumbnailChanged = $state(eventData === null && (prefill?.thumbnailFile ?? null) !== null);
|
|
57
|
+
|
|
58
|
+
// Initial values: prefer prefill (only honored for brand-new events) so the
|
|
59
|
+
// title editor and date inputs see the imported values on their first render
|
|
60
|
+
// — TipTap reads `content` only once at mount.
|
|
61
|
+
// svelte-ignore state_referenced_locally
|
|
62
|
+
const initialPrefill = eventData === null ? prefill : null;
|
|
63
|
+
const initialTimezone =
|
|
64
|
+
initialPrefill?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
65
|
+
|
|
66
|
+
// svelte-ignore state_referenced_locally
|
|
67
|
+
let name = $state(initialPrefill?.name ?? eventData?.name ?? '');
|
|
68
|
+
let description = $state(initialPrefill?.description ?? '');
|
|
69
|
+
let startsAt = $state(
|
|
70
|
+
initialPrefill?.startsAt ? isoToDatetimeLocalInTz(initialPrefill.startsAt, initialTimezone) : ''
|
|
71
|
+
);
|
|
72
|
+
let endsAt = $state(
|
|
73
|
+
initialPrefill?.endsAt ? isoToDatetimeLocalInTz(initialPrefill.endsAt, initialTimezone) : ''
|
|
74
|
+
);
|
|
75
|
+
let timezone = $state(initialTimezone);
|
|
76
|
+
let mode: EventMode = $state(initialPrefill?.mode ?? 'inperson');
|
|
77
|
+
let visibility: Visibility = $state('public');
|
|
78
|
+
// svelte-ignore state_referenced_locally
|
|
79
|
+
let eventTheme: EventTheme = $state(
|
|
80
|
+
eventData === null
|
|
81
|
+
? {
|
|
82
|
+
...defaultTheme,
|
|
83
|
+
accentColor: initialTheme?.accentColor ?? defaultTheme.accentColor,
|
|
84
|
+
...(initialTheme?.baseColor ? { baseColor: initialTheme.baseColor } : {})
|
|
85
|
+
}
|
|
86
|
+
: { ...defaultTheme }
|
|
87
|
+
);
|
|
88
|
+
const initialThumbnailFile = initialPrefill?.thumbnailFile ?? null;
|
|
89
|
+
let thumbnailFile: File | null = $state(initialThumbnailFile);
|
|
90
|
+
let thumbnailPreview: string | null = $state(
|
|
91
|
+
initialThumbnailFile ? URL.createObjectURL(initialThumbnailFile) : null
|
|
92
|
+
);
|
|
93
|
+
// svelte-ignore state_referenced_locally
|
|
94
|
+
let selectedPreset: string | null = $state(
|
|
95
|
+
initialThumbnailFile ? null : eventData === null ? DEFAULT_PRESET : null
|
|
96
|
+
);
|
|
97
|
+
let submitting = $state(false);
|
|
98
|
+
let error: string | null = $state(null);
|
|
99
|
+
let titleEditor = $state<Editor | null>(null);
|
|
100
|
+
|
|
101
|
+
let location: EventLocation | null = $state(
|
|
102
|
+
initialPrefill?.location ? { ...initialPrefill.location } : null
|
|
103
|
+
);
|
|
104
|
+
let locationChanged = $state(initialPrefill?.location != null);
|
|
105
|
+
|
|
106
|
+
let links: Array<{ uri: string; name: string }> = $state(
|
|
107
|
+
initialPrefill?.links ? initialPrefill.links.map((l) => ({ ...l })) : []
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let showRecurringModal = $state(false);
|
|
111
|
+
|
|
112
|
+
function populateLocationFromEventData() {
|
|
113
|
+
if (!eventData) return;
|
|
114
|
+
if (eventData.locations && eventData.locations.length > 0) {
|
|
115
|
+
const loc = eventData.locations.find(
|
|
116
|
+
(v) => v.$type === 'community.lexicon.location.address'
|
|
117
|
+
) as { street?: string; locality?: string; region?: string; country?: string } | undefined;
|
|
118
|
+
if (loc) {
|
|
119
|
+
const street = loc.street || undefined;
|
|
120
|
+
const locality = loc.locality || undefined;
|
|
121
|
+
const region = loc.region || undefined;
|
|
122
|
+
const country = loc.country || undefined;
|
|
123
|
+
location = {
|
|
124
|
+
...(street && { street }),
|
|
125
|
+
...(locality && { locality }),
|
|
126
|
+
...(region && { region }),
|
|
127
|
+
...(country && { country })
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
locationChanged = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function populateThumbnailFromEventData() {
|
|
135
|
+
if (!eventData) return;
|
|
136
|
+
if (eventData.media && eventData.media.length > 0) {
|
|
137
|
+
const media = getEventThumbnail(eventData.media);
|
|
138
|
+
if (media?.content) {
|
|
139
|
+
const url = getCDNImageBlobUrl({ did: actorDid, blob: media.content });
|
|
140
|
+
if (url) {
|
|
141
|
+
thumbnailPreview = url;
|
|
142
|
+
thumbnailChanged = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function populateFromEventData() {
|
|
149
|
+
if (!eventData) return;
|
|
150
|
+
name = eventData.name || '';
|
|
151
|
+
description = eventData.description || '';
|
|
152
|
+
// Restore the event's authored timezone first so the wall-clock fields we
|
|
153
|
+
// populate below land in that zone (not the viewer's browser zone).
|
|
154
|
+
if (eventData.timezone) timezone = eventData.timezone;
|
|
155
|
+
startsAt = eventData.startsAt ? isoToDatetimeLocalInTz(eventData.startsAt, timezone) : '';
|
|
156
|
+
endsAt = eventData.endsAt ? isoToDatetimeLocalInTz(eventData.endsAt, timezone) : '';
|
|
157
|
+
mode = eventData.mode ? stripModePrefix(eventData.mode) : 'inperson';
|
|
158
|
+
const prefs = (eventData as unknown as { preferences?: { showInDiscovery?: boolean } })
|
|
159
|
+
.preferences;
|
|
160
|
+
if (prefs && prefs.showInDiscovery === false) visibility = 'unlisted';
|
|
161
|
+
else visibility = 'public';
|
|
162
|
+
links = eventData.uris ? eventData.uris.map((l) => ({ uri: l.uri, name: l.name || '' })) : [];
|
|
163
|
+
if (eventData.theme) eventTheme = { ...eventData.theme };
|
|
164
|
+
populateLocationFromEventData();
|
|
165
|
+
populateThumbnailFromEventData();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
onMount(() => {
|
|
169
|
+
if (!isNew) populateFromEventData();
|
|
170
|
+
titleEditor?.commands.focus();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
let hostName = $derived(viewer.displayName || viewer.handle || viewer.did || '');
|
|
174
|
+
|
|
175
|
+
let thumbnailDateStr = $derived.by(() => {
|
|
176
|
+
if (!startsAt) return '';
|
|
177
|
+
const d = new Date(startsAt);
|
|
178
|
+
if (isNaN(d.getTime())) return '';
|
|
179
|
+
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Trim a CalendarDateTime.toString() ("YYYY-MM-DDTHH:mm:ss[.sss]") down to
|
|
183
|
+
// the "YYYY-MM-DDTHH:mm" shape that <input type="datetime-local"> expects.
|
|
184
|
+
function cdtToDatetimeLocal(s: string): string {
|
|
185
|
+
return s.slice(0, 16);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Auto-set end date to 1 hour after start if empty
|
|
189
|
+
$effect(() => {
|
|
190
|
+
if (startsAt && !endsAt) {
|
|
191
|
+
endsAt = cdtToDatetimeLocal(parseDateTime(startsAt).add({ hours: 1 }).toString());
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Auto-adjust end date if start moves past it
|
|
196
|
+
$effect(() => {
|
|
197
|
+
if (startsAt && endsAt) {
|
|
198
|
+
const s = parseDateTime(startsAt);
|
|
199
|
+
const e = parseDateTime(endsAt);
|
|
200
|
+
if (s.compare(e) >= 0) {
|
|
201
|
+
endsAt = cdtToDatetimeLocal(s.add({ hours: 1 }).toString());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
async function handleSubmit() {
|
|
207
|
+
error = null;
|
|
208
|
+
|
|
209
|
+
if (!name.trim()) return void (error = 'Name is required.');
|
|
210
|
+
if (!startsAt) return void (error = 'Start date is required.');
|
|
211
|
+
if (!endsAt) return void (error = 'End date is required.');
|
|
212
|
+
if (!viewer.isLoggedIn || !viewer.did) return void (error = 'You must be logged in.');
|
|
213
|
+
|
|
214
|
+
submitting = true;
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
// Generate thumbnail from preset if selected and no custom upload
|
|
218
|
+
if (selectedPreset && !thumbnailFile) {
|
|
219
|
+
const rendered = await renderPresetThumbnail({
|
|
220
|
+
design: selectedPreset,
|
|
221
|
+
seed: hashSeed(rkey),
|
|
222
|
+
name,
|
|
223
|
+
dateStr: thumbnailDateStr,
|
|
224
|
+
accent: eventTheme.accentColor
|
|
225
|
+
});
|
|
226
|
+
if (rendered) {
|
|
227
|
+
thumbnailFile = rendered;
|
|
228
|
+
thumbnailChanged = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>;
|
|
233
|
+
const media = await buildThumbnailMedia({
|
|
234
|
+
isNew,
|
|
235
|
+
thumbnailChanged,
|
|
236
|
+
thumbnailFile,
|
|
237
|
+
existingMedia,
|
|
238
|
+
uploadBlob: (blob) =>
|
|
239
|
+
adapter.uploadBlob(blob) as unknown as Promise<Record<string, unknown>>
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const record = await buildEventRecord({
|
|
243
|
+
eventData,
|
|
244
|
+
isNew,
|
|
245
|
+
name,
|
|
246
|
+
description,
|
|
247
|
+
startsAt,
|
|
248
|
+
endsAt,
|
|
249
|
+
timezone,
|
|
250
|
+
mode,
|
|
251
|
+
visibility,
|
|
252
|
+
theme: eventTheme,
|
|
253
|
+
links,
|
|
254
|
+
location,
|
|
255
|
+
locationChanged,
|
|
256
|
+
media,
|
|
257
|
+
resolveHandle: (handle) => adapter.resolveHandle(handle)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (isNew && prefill?.additionalData) {
|
|
261
|
+
const existing = (record.additionalData ?? {}) as Record<string, unknown>;
|
|
262
|
+
record.additionalData = { ...existing, ...prefill.additionalData };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const result = await adapter.putRecord({
|
|
266
|
+
collection: 'community.lexicon.calendar.event',
|
|
267
|
+
rkey,
|
|
268
|
+
record
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await adapter.notifyUpdate?.(result.uri);
|
|
272
|
+
adapter.onSaved({ uri: result.uri, rkey, isNew });
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error(`Failed to ${isNew ? 'create' : 'save'} event:`, e);
|
|
275
|
+
error =
|
|
276
|
+
e instanceof LocationValidationError
|
|
277
|
+
? e.message
|
|
278
|
+
: `Failed to ${isNew ? 'create' : 'save'} event. Please try again.`;
|
|
279
|
+
} finally {
|
|
280
|
+
submitting = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let showDeleteConfirm = $state(false);
|
|
285
|
+
let deleting = $state(false);
|
|
286
|
+
|
|
287
|
+
async function handleDelete() {
|
|
288
|
+
deleting = true;
|
|
289
|
+
try {
|
|
290
|
+
await adapter.deleteRecord({
|
|
291
|
+
collection: 'community.lexicon.calendar.event',
|
|
292
|
+
rkey
|
|
293
|
+
});
|
|
294
|
+
const eventUri = `at://${viewer.did}/community.lexicon.calendar.event/${rkey}`;
|
|
295
|
+
await adapter.notifyUpdate?.(eventUri);
|
|
296
|
+
adapter.onDeleted?.();
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.error('Failed to delete event:', e);
|
|
299
|
+
error = 'Failed to delete event. Please try again.';
|
|
300
|
+
} finally {
|
|
301
|
+
deleting = false;
|
|
302
|
+
showDeleteConfirm = false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
</script>
|
|
306
|
+
|
|
307
|
+
<ThemeApply accentColor={eventTheme.accentColor} baseColor={eventTheme.baseColor} />
|
|
308
|
+
<div class="motion-reduce:hidden"><ThemeBackground theme={eventTheme} /></div>
|
|
309
|
+
|
|
310
|
+
<div class="px-6 py-12 sm:py-12">
|
|
311
|
+
<div class="mx-auto max-w-3xl">
|
|
312
|
+
{#if !viewer.isLoggedIn}
|
|
313
|
+
<div
|
|
314
|
+
class="rounded-2xl border border-base-200 bg-base-100 p-8 text-center dark:border-base-800 dark:bg-base-900/50"
|
|
315
|
+
>
|
|
316
|
+
<p class="mb-4 text-base-600 dark:text-base-400">
|
|
317
|
+
Log in to {isNew ? 'create an event' : 'edit this event'}.
|
|
318
|
+
</p>
|
|
319
|
+
<Button onclick={() => adapter.requestLogin()}>Log in</Button>
|
|
320
|
+
</div>
|
|
321
|
+
{:else}
|
|
322
|
+
<form
|
|
323
|
+
onsubmit={(e) => {
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
handleSubmit();
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
<!-- Two-column layout mirroring detail page -->
|
|
329
|
+
<div
|
|
330
|
+
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]"
|
|
331
|
+
>
|
|
332
|
+
<ThumbnailSection
|
|
333
|
+
{rkey}
|
|
334
|
+
{name}
|
|
335
|
+
dateStr={thumbnailDateStr}
|
|
336
|
+
accent={eventTheme.accentColor}
|
|
337
|
+
bind:thumbnailFile
|
|
338
|
+
bind:thumbnailPreview
|
|
339
|
+
bind:thumbnailChanged
|
|
340
|
+
bind:selectedPreset
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<!-- Right column: event details -->
|
|
344
|
+
<div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-5 md:row-start-1">
|
|
345
|
+
<!-- Name -->
|
|
346
|
+
<div class="mb-2 min-h-14">
|
|
347
|
+
<InlineEditor
|
|
348
|
+
bind:value={name}
|
|
349
|
+
bind:editor={titleEditor}
|
|
350
|
+
label="Event name"
|
|
351
|
+
singleLine
|
|
352
|
+
placeholder="Event name"
|
|
353
|
+
class="w-full text-3xl leading-tight font-semibold tracking-tight text-base-900 placeholder:text-base-500 focus-within:outline-none focus:outline-none sm:text-4xl dark:text-base-50 dark:placeholder:text-base-500"
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<!-- Mode toggle -->
|
|
358
|
+
<div class="mb-3">
|
|
359
|
+
<ToggleGroup
|
|
360
|
+
type="single"
|
|
361
|
+
bind:value={
|
|
362
|
+
() => mode,
|
|
363
|
+
(val) => {
|
|
364
|
+
if (val) mode = val;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
class="w-fit"
|
|
368
|
+
size="xs"
|
|
369
|
+
>
|
|
370
|
+
<ToggleGroupItem value="inperson">In Person</ToggleGroupItem>
|
|
371
|
+
<ToggleGroupItem value="virtual">Virtual</ToggleGroupItem>
|
|
372
|
+
<ToggleGroupItem value="hybrid">Hybrid</ToggleGroupItem>
|
|
373
|
+
</ToggleGroup>
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
<!-- Visibility toggle -->
|
|
377
|
+
<div class="mb-8">
|
|
378
|
+
<ToggleGroup
|
|
379
|
+
type="single"
|
|
380
|
+
bind:value={
|
|
381
|
+
() => visibility,
|
|
382
|
+
(val) => {
|
|
383
|
+
if (val) visibility = val as Visibility;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
class="w-fit"
|
|
387
|
+
size="xs"
|
|
388
|
+
>
|
|
389
|
+
<ToggleGroupItem value="public">Public</ToggleGroupItem>
|
|
390
|
+
<ToggleGroupItem value="unlisted">Unlisted</ToggleGroupItem>
|
|
391
|
+
</ToggleGroup>
|
|
392
|
+
<div class="mt-1.5 text-xs text-muted">
|
|
393
|
+
{#if visibility === 'public'}
|
|
394
|
+
Anyone can view and it appears in discovery.
|
|
395
|
+
{:else}
|
|
396
|
+
Public to anyone with the link, but hidden from discovery.
|
|
397
|
+
{/if}
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<!-- Date row -->
|
|
402
|
+
<div class="mb-5 grid min-w-0 gap-2">
|
|
403
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
404
|
+
<span class="w-9 shrink-0 text-sm text-muted">Start</span>
|
|
405
|
+
<DateTimePicker bind:value={startsAt} label="Start" required />
|
|
406
|
+
</div>
|
|
407
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
408
|
+
<span class="w-9 shrink-0 text-sm text-muted">End</span>
|
|
409
|
+
<DateTimePicker
|
|
410
|
+
bind:value={endsAt}
|
|
411
|
+
label="End"
|
|
412
|
+
minValue={startsAt}
|
|
413
|
+
referenceTime={startsAt}
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="min-w-0 pl-11">
|
|
417
|
+
<TimezonePicker bind:value={timezone} />
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<LocationSection bind:location bind:locationChanged />
|
|
422
|
+
|
|
423
|
+
<!-- About Event -->
|
|
424
|
+
<div class="mt-8 mb-8">
|
|
425
|
+
<p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">About</p>
|
|
426
|
+
<textarea
|
|
427
|
+
aria-label="Event description"
|
|
428
|
+
bind:value={description}
|
|
429
|
+
rows={4}
|
|
430
|
+
placeholder="What's this event about? @mentions, #hashtags and links will be detected automatically."
|
|
431
|
+
class="w-full resize-none border-0 bg-transparent px-0 leading-relaxed text-base-700 placeholder:text-base-500 focus:border-0 focus:ring-0 focus:outline-none dark:text-base-300 dark:placeholder:text-base-500"
|
|
432
|
+
style="field-sizing: content;"></textarea>
|
|
433
|
+
<p class="mt-2 text-xs text-muted">
|
|
434
|
+
Add a hashtag like <span class="font-medium">#webdev</span> or
|
|
435
|
+
<span class="font-medium">#music</span>
|
|
436
|
+
to have this event appear under a
|
|
437
|
+
<a href="/topics" class="text-action underline underline-offset-2">topic</a>.
|
|
438
|
+
</p>
|
|
439
|
+
</div>
|
|
440
|
+
|
|
441
|
+
{#if error}
|
|
442
|
+
<p class="mb-4 text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
443
|
+
{/if}
|
|
444
|
+
|
|
445
|
+
<Button type="submit" disabled={submitting || !name.trim() || !startsAt || !endsAt}>
|
|
446
|
+
{submitting
|
|
447
|
+
? isNew
|
|
448
|
+
? 'Publishing...'
|
|
449
|
+
: 'Saving...'
|
|
450
|
+
: isNew
|
|
451
|
+
? 'Publish Event'
|
|
452
|
+
: 'Save Changes'}
|
|
453
|
+
</Button>
|
|
454
|
+
{#if !isNew && adapter.features.recurring}
|
|
455
|
+
<Button
|
|
456
|
+
type="button"
|
|
457
|
+
variant="secondary"
|
|
458
|
+
disabled={submitting || !name.trim() || !startsAt || !endsAt}
|
|
459
|
+
onclick={() => (showRecurringModal = true)}
|
|
460
|
+
>
|
|
461
|
+
Add recurring events
|
|
462
|
+
</Button>
|
|
463
|
+
{/if}
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<!-- Hosted By -->
|
|
467
|
+
<div class="order-3 md:order-0 md:col-start-1">
|
|
468
|
+
<p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Hosted By</p>
|
|
469
|
+
<div class="flex items-center gap-2.5">
|
|
470
|
+
<Avatar src={viewer.avatar} name={hostName} class="size-8 shrink-0" />
|
|
471
|
+
<span class="truncate text-sm font-medium text-base-900 dark:text-base-100">
|
|
472
|
+
{hostName}
|
|
473
|
+
</span>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<div class="order-4 space-y-6 md:order-0 md:col-start-1">
|
|
478
|
+
<LinksSection bind:links {adapter} {viewer} />
|
|
479
|
+
<ThemeSection bind:theme={eventTheme} />
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{#if !isNew && adapter.features.delete}
|
|
484
|
+
<div class="mt-12 border-t border-base-200 pt-8 dark:border-base-800">
|
|
485
|
+
{#if showDeleteConfirm}
|
|
486
|
+
<div class="flex items-center gap-3">
|
|
487
|
+
<p class="text-sm text-red-600 dark:text-red-400">
|
|
488
|
+
Are you sure? This cannot be undone.
|
|
489
|
+
</p>
|
|
490
|
+
<Button
|
|
491
|
+
variant="secondary"
|
|
492
|
+
size="sm"
|
|
493
|
+
onclick={() => (showDeleteConfirm = false)}
|
|
494
|
+
disabled={deleting}
|
|
495
|
+
>
|
|
496
|
+
Cancel
|
|
497
|
+
</Button>
|
|
498
|
+
<Button
|
|
499
|
+
size="sm"
|
|
500
|
+
onclick={handleDelete}
|
|
501
|
+
disabled={deleting}
|
|
502
|
+
variant="primary"
|
|
503
|
+
class="red"
|
|
504
|
+
>
|
|
505
|
+
{deleting ? 'Deleting...' : 'Delete'}
|
|
506
|
+
</Button>
|
|
507
|
+
</div>
|
|
508
|
+
{:else}
|
|
509
|
+
<Button variant="primary" class="red" onclick={() => (showDeleteConfirm = true)}
|
|
510
|
+
>Delete event</Button
|
|
511
|
+
>
|
|
512
|
+
{/if}
|
|
513
|
+
</div>
|
|
514
|
+
{/if}
|
|
515
|
+
</form>
|
|
516
|
+
{/if}
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<RecurringModal
|
|
521
|
+
bind:open={showRecurringModal}
|
|
522
|
+
{rkey}
|
|
523
|
+
{eventData}
|
|
524
|
+
{isNew}
|
|
525
|
+
{name}
|
|
526
|
+
{startsAt}
|
|
527
|
+
{endsAt}
|
|
528
|
+
{mode}
|
|
529
|
+
{timezone}
|
|
530
|
+
{description}
|
|
531
|
+
{links}
|
|
532
|
+
{location}
|
|
533
|
+
{thumbnailDateStr}
|
|
534
|
+
{thumbnailFile}
|
|
535
|
+
{thumbnailChanged}
|
|
536
|
+
{selectedPreset}
|
|
537
|
+
{adapter}
|
|
538
|
+
{viewer}
|
|
539
|
+
accent={eventTheme.accentColor}
|
|
540
|
+
/>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FlatEventRecord } from './contrail.js';
|
|
2
|
+
import { type EventTheme } from './theme.js';
|
|
3
|
+
import { type EventEditorPrefill } from './editor/types';
|
|
4
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter';
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
eventData: FlatEventRecord | null;
|
|
7
|
+
actorDid: string;
|
|
8
|
+
rkey: string;
|
|
9
|
+
adapter: EditorAdapter;
|
|
10
|
+
viewer: EditorViewer;
|
|
11
|
+
/** Override default theme for new events (e.g. inherit embedder's palette). */
|
|
12
|
+
initialTheme?: Partial<EventTheme>;
|
|
13
|
+
/** Autofill payload for new events (e.g. imported from Luma/Meetup). */
|
|
14
|
+
prefill?: EventEditorPrefill | null;
|
|
15
|
+
};
|
|
16
|
+
declare const EventEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type EventEditor = ReturnType<typeof EventEditor>;
|
|
18
|
+
export default EventEditor;
|