@atmo-dev/kit 0.3.0 → 0.5.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 +94 -1
- package/dist/components/ExternalEmbed.svelte +174 -0
- package/dist/components/ExternalEmbed.svelte.d.ts +8 -0
- package/dist/components/GrainAlbumPicker.svelte +218 -0
- package/dist/components/GrainAlbumPicker.svelte.d.ts +14 -0
- package/dist/components/GrainAlbumView.svelte +94 -0
- package/dist/components/GrainAlbumView.svelte.d.ts +10 -0
- package/dist/components/NestedComments.svelte +20 -10
- package/dist/components/NestedComments.svelte.d.ts +2 -0
- package/dist/components/PhotoGrid.svelte +53 -0
- package/dist/components/PhotoGrid.svelte.d.ts +9 -0
- package/dist/components/PhotoLightbox.svelte +332 -0
- package/dist/components/PhotoLightbox.svelte.d.ts +12 -0
- package/dist/components/PostContent.svelte +78 -20
- package/dist/components/PostContent.svelte.d.ts +5 -0
- package/dist/components/StreamplacePicker.svelte +94 -0
- package/dist/components/StreamplacePicker.svelte.d.ts +12 -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/grain/types.d.ts +41 -0
- package/dist/grain/types.js +1 -0
- package/dist/grain/urls.d.ts +7 -0
- package/dist/grain/urls.js +74 -0
- package/dist/grain/xrpc.d.ts +3 -0
- package/dist/grain/xrpc.js +129 -0
- package/dist/grain.d.ts +3 -0
- package/dist/grain.js +2 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +9 -0
- package/dist/media.d.ts +10 -0
- package/dist/media.js +1 -0
- package/dist/social/external-embeds.d.ts +39 -0
- package/dist/social/external-embeds.js +107 -0
- package/dist/social/posts.d.ts +59 -17
- package/dist/social/posts.js +99 -12
- package/dist/social.d.ts +2 -0
- package/dist/social.js +1 -0
- package/dist/streamplace.d.ts +6 -0
- package/dist/streamplace.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Checkbox, Input, Modal, ToggleGroup, ToggleGroupItem } from '../../index.js';
|
|
3
|
+
import { parseDateTime } from '@internationalized/date';
|
|
4
|
+
import * as TID from '@atcute/tid';
|
|
5
|
+
import type { FlatEventRecord } from '../contrail.js';
|
|
6
|
+
import type { EventLocation, EventMode } from './types';
|
|
7
|
+
import { LocationValidationError, normalizeAddressLocation } from './location';
|
|
8
|
+
import { buildThumbnailMedia, renderPresetThumbnail } from './save';
|
|
9
|
+
import { hashSeed } from '../thumbnails/designs.js';
|
|
10
|
+
import type { EditorAdapter, EditorViewer } from './adapter';
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
open = $bindable(),
|
|
14
|
+
rkey,
|
|
15
|
+
eventData,
|
|
16
|
+
isNew,
|
|
17
|
+
name,
|
|
18
|
+
startsAt,
|
|
19
|
+
endsAt,
|
|
20
|
+
mode,
|
|
21
|
+
timezone,
|
|
22
|
+
description,
|
|
23
|
+
links,
|
|
24
|
+
location,
|
|
25
|
+
thumbnailDateStr,
|
|
26
|
+
thumbnailFile,
|
|
27
|
+
thumbnailChanged,
|
|
28
|
+
selectedPreset,
|
|
29
|
+
accent,
|
|
30
|
+
adapter,
|
|
31
|
+
viewer
|
|
32
|
+
}: {
|
|
33
|
+
open: boolean;
|
|
34
|
+
rkey: string;
|
|
35
|
+
eventData: FlatEventRecord | null;
|
|
36
|
+
isNew: boolean;
|
|
37
|
+
name: string;
|
|
38
|
+
startsAt: string;
|
|
39
|
+
endsAt: string;
|
|
40
|
+
mode: EventMode;
|
|
41
|
+
timezone: string;
|
|
42
|
+
description: string;
|
|
43
|
+
links: Array<{ uri: string; name: string }>;
|
|
44
|
+
location: EventLocation | null;
|
|
45
|
+
thumbnailDateStr: string;
|
|
46
|
+
thumbnailFile: File | null;
|
|
47
|
+
thumbnailChanged: boolean;
|
|
48
|
+
selectedPreset: string | null;
|
|
49
|
+
accent: string;
|
|
50
|
+
adapter: EditorAdapter;
|
|
51
|
+
viewer: EditorViewer;
|
|
52
|
+
} = $props();
|
|
53
|
+
|
|
54
|
+
let interval = $state(1);
|
|
55
|
+
let unit: 'days' | 'weeks' | 'months' | 'years' = $state('weeks');
|
|
56
|
+
let count = $state(4);
|
|
57
|
+
let numberInTitle = $state(false);
|
|
58
|
+
let creating = $state(false);
|
|
59
|
+
let errorMsg: string | null = $state(null);
|
|
60
|
+
let created = $state(0);
|
|
61
|
+
let validRecurrence = $derived(
|
|
62
|
+
Number.isInteger(count) &&
|
|
63
|
+
count >= 1 &&
|
|
64
|
+
count <= 52 &&
|
|
65
|
+
Number.isInteger(interval) &&
|
|
66
|
+
interval >= 1 &&
|
|
67
|
+
interval <= 99
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
let titleNumberMatch = $derived(name.match(/#?(\d+)\s*$/));
|
|
71
|
+
let detectedStartNumber = $derived(titleNumberMatch ? parseInt(titleNumberMatch[1]) : null);
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (detectedStartNumber !== null) numberInTitle = true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
async function handleCreate() {
|
|
78
|
+
if (
|
|
79
|
+
creating ||
|
|
80
|
+
!validRecurrence ||
|
|
81
|
+
!name.trim() ||
|
|
82
|
+
!startsAt ||
|
|
83
|
+
!viewer.isLoggedIn ||
|
|
84
|
+
!viewer.did
|
|
85
|
+
)
|
|
86
|
+
return;
|
|
87
|
+
const copies = count;
|
|
88
|
+
const period = interval;
|
|
89
|
+
|
|
90
|
+
creating = true;
|
|
91
|
+
errorMsg = null;
|
|
92
|
+
created = 0;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const normalizedLocation = location ? normalizeAddressLocation(location) : null;
|
|
96
|
+
// Recurring instances advance by wall-clock duration (e.g. "every week
|
|
97
|
+
// at 10am"), so operate on CalendarDateTime — not absolute instants —
|
|
98
|
+
// to preserve the wall time across DST transitions.
|
|
99
|
+
const baseStart = parseDateTime(startsAt);
|
|
100
|
+
const baseEnd = endsAt ? parseDateTime(endsAt) : null;
|
|
101
|
+
const durationMs = baseEnd
|
|
102
|
+
? baseEnd.toDate(timezone).getTime() - baseStart.toDate(timezone).getTime()
|
|
103
|
+
: 0;
|
|
104
|
+
const baseName =
|
|
105
|
+
numberInTitle && titleNumberMatch ? name.replace(/#?\d+\s*$/, '').trimEnd() : name.trim();
|
|
106
|
+
const startNum = detectedStartNumber ?? 1;
|
|
107
|
+
const hasHash = titleNumberMatch ? titleNumberMatch[0].includes('#') : false;
|
|
108
|
+
|
|
109
|
+
// Generate thumbnail from preset if selected and no custom upload.
|
|
110
|
+
let fileForUpload = thumbnailFile;
|
|
111
|
+
let hasNewThumbnail = thumbnailChanged;
|
|
112
|
+
if (selectedPreset && !fileForUpload) {
|
|
113
|
+
const rendered = await renderPresetThumbnail({
|
|
114
|
+
design: selectedPreset,
|
|
115
|
+
seed: hashSeed(rkey),
|
|
116
|
+
name,
|
|
117
|
+
dateStr: thumbnailDateStr,
|
|
118
|
+
accent
|
|
119
|
+
});
|
|
120
|
+
if (rendered) {
|
|
121
|
+
fileForUpload = rendered;
|
|
122
|
+
hasNewThumbnail = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>;
|
|
127
|
+
const media = await buildThumbnailMedia({
|
|
128
|
+
isNew,
|
|
129
|
+
thumbnailChanged: hasNewThumbnail,
|
|
130
|
+
thumbnailFile: fileForUpload,
|
|
131
|
+
existingMedia,
|
|
132
|
+
uploadBlob: (blob) =>
|
|
133
|
+
adapter.uploadBlob(blob) as unknown as Promise<Record<string, unknown>>
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const parentUri = `at://${viewer.did}/community.lexicon.calendar.event/${rkey}`;
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < copies; i++) {
|
|
139
|
+
const offset = i + 1;
|
|
140
|
+
const step = offset * period;
|
|
141
|
+
const eventStart =
|
|
142
|
+
unit === 'days'
|
|
143
|
+
? baseStart.add({ days: step })
|
|
144
|
+
: unit === 'weeks'
|
|
145
|
+
? baseStart.add({ weeks: step })
|
|
146
|
+
: unit === 'months'
|
|
147
|
+
? baseStart.add({ months: step })
|
|
148
|
+
: baseStart.add({ years: step });
|
|
149
|
+
|
|
150
|
+
const eventStartIso = eventStart.toDate(timezone).toISOString();
|
|
151
|
+
// Preserve the original absolute duration (handles events that
|
|
152
|
+
// span midnight or odd wall-clock lengths correctly).
|
|
153
|
+
const eventEndIso = durationMs
|
|
154
|
+
? new Date(eventStart.toDate(timezone).getTime() + durationMs).toISOString()
|
|
155
|
+
: null;
|
|
156
|
+
|
|
157
|
+
let eventName = baseName;
|
|
158
|
+
if (numberInTitle) {
|
|
159
|
+
const num = startNum + (i + 1);
|
|
160
|
+
eventName = hasHash ? `${baseName} #${num}` : `${baseName} ${num}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const newRkey = TID.now();
|
|
164
|
+
const record: Record<string, unknown> = {
|
|
165
|
+
$type: 'community.lexicon.calendar.event',
|
|
166
|
+
createdWith: 'https://atmo.rsvp',
|
|
167
|
+
name: eventName,
|
|
168
|
+
mode: `community.lexicon.calendar.event#${mode}`,
|
|
169
|
+
status: 'community.lexicon.calendar.event#scheduled',
|
|
170
|
+
startsAt: eventStartIso,
|
|
171
|
+
timezone,
|
|
172
|
+
createdAt: new Date().toISOString(),
|
|
173
|
+
recurringEventOf: parentUri
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const trimmedDescription = description.trim();
|
|
177
|
+
if (trimmedDescription) record.description = trimmedDescription;
|
|
178
|
+
if (eventEndIso) record.endsAt = eventEndIso;
|
|
179
|
+
if (media) record.media = media;
|
|
180
|
+
if (links.length > 0) record.uris = links;
|
|
181
|
+
if (normalizedLocation) {
|
|
182
|
+
record.locations = [
|
|
183
|
+
{
|
|
184
|
+
$type: 'community.lexicon.location.address',
|
|
185
|
+
...normalizedLocation
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const result = await adapter.putRecord({
|
|
192
|
+
collection: 'community.lexicon.calendar.event',
|
|
193
|
+
rkey: newRkey,
|
|
194
|
+
record
|
|
195
|
+
});
|
|
196
|
+
await adapter.notifyUpdate?.(result.uri);
|
|
197
|
+
created = i + 1;
|
|
198
|
+
} catch {
|
|
199
|
+
errorMsg = `Failed to create event ${i + 1}. Stopping.`;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
open = false;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('Failed to create recurring events:', e);
|
|
207
|
+
errorMsg =
|
|
208
|
+
e instanceof LocationValidationError
|
|
209
|
+
? e.message
|
|
210
|
+
: 'Failed to create recurring events. Please try again.';
|
|
211
|
+
} finally {
|
|
212
|
+
creating = false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<Modal bind:open title="Recurring events" dismissible={!creating}>
|
|
218
|
+
<p class="mt-1 text-sm text-muted">Create multiple copies of this event at regular intervals.</p>
|
|
219
|
+
|
|
220
|
+
<div class="mt-4 space-y-4">
|
|
221
|
+
<div>
|
|
222
|
+
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
223
|
+
<label class="mb-1 block text-sm font-medium text-base-700 dark:text-base-300">
|
|
224
|
+
Number of events to create
|
|
225
|
+
</label>
|
|
226
|
+
<Input
|
|
227
|
+
type="number"
|
|
228
|
+
aria-label="Number of events to create"
|
|
229
|
+
bind:value={count}
|
|
230
|
+
disabled={creating}
|
|
231
|
+
min={1}
|
|
232
|
+
max={52}
|
|
233
|
+
class="w-24"
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div>
|
|
238
|
+
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
239
|
+
<label class="mb-1 block text-sm font-medium text-base-700 dark:text-base-300">
|
|
240
|
+
Repeat every
|
|
241
|
+
</label>
|
|
242
|
+
<div class="flex items-center gap-2">
|
|
243
|
+
<Input
|
|
244
|
+
type="number"
|
|
245
|
+
aria-label="Repeat interval"
|
|
246
|
+
bind:value={interval}
|
|
247
|
+
disabled={creating}
|
|
248
|
+
min={1}
|
|
249
|
+
max={99}
|
|
250
|
+
class="w-20"
|
|
251
|
+
/>
|
|
252
|
+
<ToggleGroup
|
|
253
|
+
type="single"
|
|
254
|
+
disabled={creating}
|
|
255
|
+
bind:value={
|
|
256
|
+
() => unit,
|
|
257
|
+
(value) => {
|
|
258
|
+
if (value) unit = value as typeof unit;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
>
|
|
262
|
+
<ToggleGroupItem value="days">days</ToggleGroupItem>
|
|
263
|
+
<ToggleGroupItem value="weeks">weeks</ToggleGroupItem>
|
|
264
|
+
<ToggleGroupItem value="months">months</ToggleGroupItem>
|
|
265
|
+
<ToggleGroupItem value="years">years</ToggleGroupItem>
|
|
266
|
+
</ToggleGroup>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div>
|
|
271
|
+
<div class="flex items-center gap-2">
|
|
272
|
+
<Checkbox bind:checked={numberInTitle} disabled={creating} aria-label="Number in title" />
|
|
273
|
+
<span class="text-sm font-medium text-base-700 dark:text-base-300">Number in title</span>
|
|
274
|
+
</div>
|
|
275
|
+
<p class="mt-1 text-xs text-muted">
|
|
276
|
+
{#if numberInTitle && detectedStartNumber !== null}
|
|
277
|
+
Titles will count up from #{detectedStartNumber + 1}
|
|
278
|
+
{:else if numberInTitle}
|
|
279
|
+
A number will be appended to each title
|
|
280
|
+
{:else}
|
|
281
|
+
Append a number to each event title
|
|
282
|
+
{/if}
|
|
283
|
+
</p>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{#if errorMsg}
|
|
288
|
+
<p role="alert" class="mt-4 text-sm text-red-600 dark:text-red-400">{errorMsg}</p>
|
|
289
|
+
{/if}
|
|
290
|
+
|
|
291
|
+
{#if creating && created > 0}
|
|
292
|
+
<p class="mt-4 text-sm text-muted">
|
|
293
|
+
Created {created} of {count} events...
|
|
294
|
+
</p>
|
|
295
|
+
{/if}
|
|
296
|
+
|
|
297
|
+
{#if created > 0 && !creating}
|
|
298
|
+
<p class="mt-4 text-sm text-action">
|
|
299
|
+
Successfully created {created} recurring events!
|
|
300
|
+
</p>
|
|
301
|
+
{/if}
|
|
302
|
+
|
|
303
|
+
<div class="mt-4 flex justify-end gap-2">
|
|
304
|
+
<Button variant="secondary" onclick={() => (open = false)} disabled={creating}>
|
|
305
|
+
{created > 0 && !creating ? 'Close' : 'Cancel'}
|
|
306
|
+
</Button>
|
|
307
|
+
{#if !created || creating}
|
|
308
|
+
<Button onclick={handleCreate} disabled={creating || !validRecurrence}>
|
|
309
|
+
{creating ? `Creating...` : `Create ${count} event${count === 1 ? '' : 's'}`}
|
|
310
|
+
</Button>
|
|
311
|
+
{/if}
|
|
312
|
+
</div>
|
|
313
|
+
</Modal>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FlatEventRecord } from '../contrail.js';
|
|
2
|
+
import type { EventLocation, EventMode } from './types';
|
|
3
|
+
import type { EditorAdapter, EditorViewer } from './adapter';
|
|
4
|
+
type $$ComponentProps = {
|
|
5
|
+
open: boolean;
|
|
6
|
+
rkey: string;
|
|
7
|
+
eventData: FlatEventRecord | null;
|
|
8
|
+
isNew: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
startsAt: string;
|
|
11
|
+
endsAt: string;
|
|
12
|
+
mode: EventMode;
|
|
13
|
+
timezone: string;
|
|
14
|
+
description: string;
|
|
15
|
+
links: Array<{
|
|
16
|
+
uri: string;
|
|
17
|
+
name: string;
|
|
18
|
+
}>;
|
|
19
|
+
location: EventLocation | null;
|
|
20
|
+
thumbnailDateStr: string;
|
|
21
|
+
thumbnailFile: File | null;
|
|
22
|
+
thumbnailChanged: boolean;
|
|
23
|
+
selectedPreset: string | null;
|
|
24
|
+
accent: string;
|
|
25
|
+
adapter: EditorAdapter;
|
|
26
|
+
viewer: EditorViewer;
|
|
27
|
+
};
|
|
28
|
+
declare const RecurringModal: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
29
|
+
type RecurringModal = ReturnType<typeof RecurringModal>;
|
|
30
|
+
export default RecurringModal;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Palette } from '@sketchyicons/svelte';
|
|
3
|
+
import { Button, Icon, Modal } from '../../index.js';
|
|
4
|
+
import ThemePicker from '../ThemePicker.svelte';
|
|
5
|
+
import { themeBackgrounds, type EventTheme } from '../theme.js';
|
|
6
|
+
|
|
7
|
+
let { theme = $bindable() }: { theme: EventTheme } = $props();
|
|
8
|
+
|
|
9
|
+
let showModal = $state(false);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div>
|
|
13
|
+
<p class="mb-3 text-xs font-semibold tracking-wider text-muted uppercase">Theme</p>
|
|
14
|
+
<Button variant="secondary" size="sm" onclick={() => (showModal = true)}>
|
|
15
|
+
<Icon img={Palette} size={16} />
|
|
16
|
+
{themeBackgrounds[theme.name] || theme.name}
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<Modal bind:open={showModal} title="Event theme">
|
|
21
|
+
<div class="mt-4">
|
|
22
|
+
<ThemePicker bind:theme />
|
|
23
|
+
</div>
|
|
24
|
+
</Modal>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type EventTheme } from '../theme.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
theme: EventTheme;
|
|
4
|
+
};
|
|
5
|
+
declare const ThemeSection: import("svelte").Component<$$ComponentProps, {}, "theme">;
|
|
6
|
+
type ThemeSection = ReturnType<typeof ThemeSection>;
|
|
7
|
+
export default ThemeSection;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Image, Upload, X } from '@sketchyicons/svelte';
|
|
3
|
+
import { Button, Icon, Modal, InsetImage } from '../../index.js';
|
|
4
|
+
import Avatar from 'svelte-boring-avatars';
|
|
5
|
+
import ThumbnailPresets from '../ThumbnailPresets.svelte';
|
|
6
|
+
import { designs, hashSeed, resolveAccentColor } from '../thumbnails/designs.js';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
rkey,
|
|
10
|
+
name,
|
|
11
|
+
dateStr,
|
|
12
|
+
accent,
|
|
13
|
+
thumbnailFile = $bindable(),
|
|
14
|
+
thumbnailPreview = $bindable(),
|
|
15
|
+
thumbnailChanged = $bindable(),
|
|
16
|
+
selectedPreset = $bindable()
|
|
17
|
+
}: {
|
|
18
|
+
rkey: string;
|
|
19
|
+
name: string;
|
|
20
|
+
dateStr: string;
|
|
21
|
+
accent: string;
|
|
22
|
+
thumbnailFile: File | null;
|
|
23
|
+
thumbnailPreview: string | null;
|
|
24
|
+
thumbnailChanged: boolean;
|
|
25
|
+
selectedPreset: string | null;
|
|
26
|
+
} = $props();
|
|
27
|
+
|
|
28
|
+
let fileInput: HTMLInputElement | undefined = $state();
|
|
29
|
+
let presetPreviewCanvas: HTMLCanvasElement | undefined = $state();
|
|
30
|
+
let showModal = $state(false);
|
|
31
|
+
let isDragOver = $state(false);
|
|
32
|
+
|
|
33
|
+
const seed = $derived(hashSeed(rkey));
|
|
34
|
+
|
|
35
|
+
function setThumbnail(file: File) {
|
|
36
|
+
thumbnailFile = file;
|
|
37
|
+
thumbnailChanged = true;
|
|
38
|
+
selectedPreset = null;
|
|
39
|
+
if (thumbnailPreview) URL.revokeObjectURL(thumbnailPreview);
|
|
40
|
+
thumbnailPreview = URL.createObjectURL(file);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onFileChange(e: Event) {
|
|
44
|
+
const input = e.target as HTMLInputElement;
|
|
45
|
+
const file = input.files?.[0];
|
|
46
|
+
if (!file) return;
|
|
47
|
+
setThumbnail(file);
|
|
48
|
+
showModal = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function onDragOver(e: DragEvent) {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
isDragOver = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onDragLeave(e: DragEvent) {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
isDragOver = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function onDrop(e: DragEvent) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
isDragOver = false;
|
|
64
|
+
const file = e.dataTransfer?.files?.[0];
|
|
65
|
+
if (file?.type.startsWith('image/')) {
|
|
66
|
+
setThumbnail(file);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeThumbnail() {
|
|
71
|
+
thumbnailFile = null;
|
|
72
|
+
thumbnailChanged = true;
|
|
73
|
+
selectedPreset = null;
|
|
74
|
+
if (thumbnailPreview) {
|
|
75
|
+
URL.revokeObjectURL(thumbnailPreview);
|
|
76
|
+
thumbnailPreview = null;
|
|
77
|
+
}
|
|
78
|
+
if (fileInput) fileInput.value = '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Render preset preview canvas whenever the selection, name, date, or accent changes.
|
|
82
|
+
$effect(() => {
|
|
83
|
+
if (selectedPreset && presetPreviewCanvas && designs[selectedPreset]) {
|
|
84
|
+
const ctx = presetPreviewCanvas.getContext('2d');
|
|
85
|
+
if (!ctx) return;
|
|
86
|
+
presetPreviewCanvas.width = 800;
|
|
87
|
+
presetPreviewCanvas.height = 800;
|
|
88
|
+
designs[selectedPreset](
|
|
89
|
+
ctx,
|
|
90
|
+
800,
|
|
91
|
+
800,
|
|
92
|
+
name || 'Event',
|
|
93
|
+
dateStr,
|
|
94
|
+
seed,
|
|
95
|
+
resolveAccentColor(accent)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
102
|
+
<div
|
|
103
|
+
class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none"
|
|
104
|
+
ondragover={onDragOver}
|
|
105
|
+
ondragleave={onDragLeave}
|
|
106
|
+
ondrop={onDrop}
|
|
107
|
+
>
|
|
108
|
+
<input
|
|
109
|
+
bind:this={fileInput}
|
|
110
|
+
type="file"
|
|
111
|
+
accept="image/*"
|
|
112
|
+
onchange={onFileChange}
|
|
113
|
+
class="hidden"
|
|
114
|
+
/>
|
|
115
|
+
<div class="group relative">
|
|
116
|
+
{#if thumbnailPreview}
|
|
117
|
+
<InsetImage src={thumbnailPreview} alt="Thumbnail preview" class="w-full rounded-panel" />
|
|
118
|
+
{:else if selectedPreset && designs[selectedPreset]}
|
|
119
|
+
<div
|
|
120
|
+
class="aspect-square w-full overflow-hidden rounded-2xl border border-base-200 dark:border-base-800"
|
|
121
|
+
>
|
|
122
|
+
<canvas bind:this={presetPreviewCanvas} class="h-full w-full"></canvas>
|
|
123
|
+
</div>
|
|
124
|
+
{:else}
|
|
125
|
+
<div
|
|
126
|
+
class="aspect-square w-full overflow-hidden rounded-2xl bg-base-100 dark:bg-base-900 [&>svg]:h-full [&>svg]:w-full"
|
|
127
|
+
>
|
|
128
|
+
<Avatar
|
|
129
|
+
size={400}
|
|
130
|
+
name={rkey}
|
|
131
|
+
variant="marble"
|
|
132
|
+
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
|
|
133
|
+
square
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
onclick={() => (showModal = true)}
|
|
140
|
+
class="absolute inset-0 flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-2xl bg-black/0 text-white/0 transition-colors group-hover:bg-black/40 group-hover:text-white/90 {isDragOver
|
|
141
|
+
? 'bg-black/40 text-white/90'
|
|
142
|
+
: ''}"
|
|
143
|
+
>
|
|
144
|
+
<Icon img={Image} size={24} />
|
|
145
|
+
<span class="text-sm font-medium">Change thumbnail</span>
|
|
146
|
+
</button>
|
|
147
|
+
{#if thumbnailPreview || selectedPreset}
|
|
148
|
+
<Button
|
|
149
|
+
variant="ghost"
|
|
150
|
+
size="icon"
|
|
151
|
+
aria-label="Remove thumbnail"
|
|
152
|
+
onclick={removeThumbnail}
|
|
153
|
+
class="absolute top-2 right-2 bg-base-900/70 text-white opacity-0 transition-opacity group-hover:opacity-100 hover:bg-red-600 focus-visible:opacity-100"
|
|
154
|
+
>
|
|
155
|
+
<Icon img={X} size={14} />
|
|
156
|
+
</Button>
|
|
157
|
+
{/if}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<Modal bind:open={showModal} title="Choose thumbnail">
|
|
162
|
+
<div class="mt-4 flex max-h-[70vh] flex-col gap-6 overflow-y-auto">
|
|
163
|
+
<Button variant="secondary" class="w-full" onclick={() => fileInput?.click()}>
|
|
164
|
+
<Icon img={Upload} size={16} />
|
|
165
|
+
Upload own thumbnail
|
|
166
|
+
</Button>
|
|
167
|
+
<ThumbnailPresets
|
|
168
|
+
{name}
|
|
169
|
+
{dateStr}
|
|
170
|
+
{accent}
|
|
171
|
+
{seed}
|
|
172
|
+
bind:selected={selectedPreset}
|
|
173
|
+
onselect={() => {
|
|
174
|
+
showModal = false;
|
|
175
|
+
thumbnailPreview = null;
|
|
176
|
+
thumbnailFile = null;
|
|
177
|
+
thumbnailChanged = true;
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</Modal>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
rkey: string;
|
|
3
|
+
name: string;
|
|
4
|
+
dateStr: string;
|
|
5
|
+
accent: string;
|
|
6
|
+
thumbnailFile: File | null;
|
|
7
|
+
thumbnailPreview: string | null;
|
|
8
|
+
thumbnailChanged: boolean;
|
|
9
|
+
selectedPreset: string | null;
|
|
10
|
+
};
|
|
11
|
+
declare const ThumbnailSection: import("svelte").Component<$$ComponentProps, {}, "thumbnailFile" | "thumbnailPreview" | "thumbnailChanged" | "selectedPreset">;
|
|
12
|
+
type ThumbnailSection = ReturnType<typeof ThumbnailSection>;
|
|
13
|
+
export default ThumbnailSection;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter contract for `EventEditor`, `EventView` and their child components.
|
|
3
|
+
*
|
|
4
|
+
* The package never reaches into atproto/session/navigation directly. Consumers
|
|
5
|
+
* implement this interface (typically with their own atcute client + router)
|
|
6
|
+
* and pass it in as a prop. The atmo app provides `createInAppAdapter`; other
|
|
7
|
+
* hosts (e.g. blento) provide their own.
|
|
8
|
+
*/
|
|
9
|
+
import type { PreferredSocialClient } from '../../social.js';
|
|
10
|
+
import type { LinkedRecordsClient } from '../linked-records.js';
|
|
11
|
+
export type EditorBlobRef = {
|
|
12
|
+
$type: 'blob';
|
|
13
|
+
ref: {
|
|
14
|
+
$link: string;
|
|
15
|
+
};
|
|
16
|
+
mimeType: string;
|
|
17
|
+
size: number;
|
|
18
|
+
};
|
|
19
|
+
export type EditorViewer = {
|
|
20
|
+
isLoggedIn: boolean;
|
|
21
|
+
/** Explicit capability from the host's token grant; absent means denied. */
|
|
22
|
+
canPostToBluesky?: boolean;
|
|
23
|
+
/** The viewer's public social-client selection; absent defaults to Bluesky. */
|
|
24
|
+
preferredSocialClient?: PreferredSocialClient | null;
|
|
25
|
+
did: string | null;
|
|
26
|
+
handle?: string;
|
|
27
|
+
displayName?: string;
|
|
28
|
+
avatar?: string;
|
|
29
|
+
};
|
|
30
|
+
export type EditorAdapter = {
|
|
31
|
+
/** Optional public read transport, supplied by the host (also used in embeds). */
|
|
32
|
+
linkedRecords?: LinkedRecordsClient;
|
|
33
|
+
features: {
|
|
34
|
+
delete: boolean;
|
|
35
|
+
recurring: boolean;
|
|
36
|
+
};
|
|
37
|
+
putRecord(opts: {
|
|
38
|
+
collection: string;
|
|
39
|
+
rkey: string;
|
|
40
|
+
record: Record<string, unknown>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
uri: string;
|
|
43
|
+
}>;
|
|
44
|
+
createRecord(opts: {
|
|
45
|
+
collection: string;
|
|
46
|
+
rkey?: string;
|
|
47
|
+
record: Record<string, unknown>;
|
|
48
|
+
}): Promise<{
|
|
49
|
+
uri: string;
|
|
50
|
+
cid?: string;
|
|
51
|
+
}>;
|
|
52
|
+
deleteRecord(opts: {
|
|
53
|
+
collection: string;
|
|
54
|
+
rkey: string;
|
|
55
|
+
}): Promise<void>;
|
|
56
|
+
uploadBlob(blob: Blob): Promise<EditorBlobRef>;
|
|
57
|
+
getRecord(opts: {
|
|
58
|
+
did: string;
|
|
59
|
+
collection: string;
|
|
60
|
+
rkey: string;
|
|
61
|
+
}): Promise<{
|
|
62
|
+
value: Record<string, unknown>;
|
|
63
|
+
}>;
|
|
64
|
+
resolveHandle(handle: string): Promise<string>;
|
|
65
|
+
onSaved(result: {
|
|
66
|
+
uri: string;
|
|
67
|
+
rkey: string;
|
|
68
|
+
isNew: boolean;
|
|
69
|
+
}): void;
|
|
70
|
+
onDeleted?(): void;
|
|
71
|
+
requestLogin(): void;
|
|
72
|
+
notifyUpdate?(uri: string): Promise<void>;
|
|
73
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter contract for `EventEditor`, `EventView` and their child components.
|
|
3
|
+
*
|
|
4
|
+
* The package never reaches into atproto/session/navigation directly. Consumers
|
|
5
|
+
* implement this interface (typically with their own atcute client + router)
|
|
6
|
+
* and pass it in as a prop. The atmo app provides `createInAppAdapter`; other
|
|
7
|
+
* hosts (e.g. blento) provide their own.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EventData } from '../event-types.js';
|
|
2
|
+
export declare function normalizeCountryCode(value: unknown): string | undefined;
|
|
3
|
+
export declare class LocationValidationError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare function normalizeAddressLocation<T extends {
|
|
7
|
+
country?: unknown;
|
|
8
|
+
}>(location: T): T & {
|
|
9
|
+
country: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function normalizeEventLocations(locations: NonNullable<EventData['locations']>): (import("../event-types.js").EventLocationVariant | ({
|
|
12
|
+
country: unknown;
|
|
13
|
+
$type: string;
|
|
14
|
+
} & {
|
|
15
|
+
country: string;
|
|
16
|
+
}))[];
|