@atmo-dev/events-ui 0.1.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/dist/DatePicker.svelte +231 -0
- package/dist/DatePicker.svelte.d.ts +11 -0
- package/dist/DateTimePicker.svelte +101 -0
- package/dist/DateTimePicker.svelte.d.ts +9 -0
- package/dist/EventAttendees.svelte +203 -0
- package/dist/EventAttendees.svelte.d.ts +13 -0
- package/dist/EventCard.svelte +131 -0
- package/dist/EventCard.svelte.d.ts +8 -0
- package/dist/EventComments.svelte +99 -0
- package/dist/EventComments.svelte.d.ts +6 -0
- package/dist/EventEditor.svelte +589 -0
- package/dist/EventEditor.svelte.d.ts +20 -0
- package/dist/EventRsvp.svelte +237 -0
- package/dist/EventRsvp.svelte.d.ts +17 -0
- package/dist/EventView.svelte +433 -0
- package/dist/EventView.svelte.d.ts +16 -0
- package/dist/ImageDropper.svelte +66 -0
- package/dist/ImageDropper.svelte.d.ts +7 -0
- package/dist/Map.svelte +27 -0
- package/dist/Map.svelte.d.ts +8 -0
- package/dist/PostToBlueskyModal.svelte +244 -0
- package/dist/PostToBlueskyModal.svelte.d.ts +22 -0
- package/dist/ShareModal.svelte +160 -0
- package/dist/ShareModal.svelte.d.ts +23 -0
- package/dist/ThemeApply.svelte +50 -0
- package/dist/ThemeApply.svelte.d.ts +7 -0
- package/dist/ThemeBackground.svelte +33 -0
- package/dist/ThemeBackground.svelte.d.ts +7 -0
- package/dist/ThemePicker.svelte +102 -0
- package/dist/ThemePicker.svelte.d.ts +7 -0
- package/dist/ThumbnailPresets.svelte +68 -0
- package/dist/ThumbnailPresets.svelte.d.ts +11 -0
- package/dist/TimePicker.svelte +188 -0
- package/dist/TimePicker.svelte.d.ts +9 -0
- package/dist/TimezonePicker.svelte +132 -0
- package/dist/TimezonePicker.svelte.d.ts +6 -0
- package/dist/VodPlayer.svelte +137 -0
- package/dist/VodPlayer.svelte.d.ts +14 -0
- package/dist/VodTranscript.svelte +72 -0
- package/dist/VodTranscript.svelte.d.ts +8 -0
- package/dist/atproto-helpers.d.ts +21 -0
- package/dist/atproto-helpers.js +61 -0
- package/dist/cal/helper.d.ts +1 -0
- package/dist/cal/helper.js +20 -0
- package/dist/cal/ical.d.ts +22 -0
- package/dist/cal/ical.js +188 -0
- package/dist/cal/sanitize.d.ts +3 -0
- package/dist/cal/sanitize.js +25 -0
- package/dist/contrail.d.ts +54 -0
- package/dist/contrail.js +22 -0
- package/dist/date-format.d.ts +22 -0
- package/dist/date-format.js +43 -0
- package/dist/editor/LinksSection.svelte +144 -0
- package/dist/editor/LinksSection.svelte.d.ts +10 -0
- package/dist/editor/LocationSection.svelte +215 -0
- package/dist/editor/LocationSection.svelte.d.ts +8 -0
- package/dist/editor/RecurringModal.svelte +270 -0
- package/dist/editor/RecurringModal.svelte.d.ts +30 -0
- package/dist/editor/ThemeSection.svelte +39 -0
- package/dist/editor/ThemeSection.svelte.d.ts +7 -0
- package/dist/editor/ThumbnailSection.svelte +219 -0
- package/dist/editor/ThumbnailSection.svelte.d.ts +13 -0
- package/dist/editor/adapter.d.ts +98 -0
- package/dist/editor/adapter.js +9 -0
- package/dist/editor/save.d.ts +42 -0
- package/dist/editor/save.js +154 -0
- package/dist/editor/types.d.ts +39 -0
- package/dist/editor/types.js +9 -0
- package/dist/event-types.d.ts +70 -0
- package/dist/event-types.js +11 -0
- package/dist/event-view/AddToCalendarButton.svelte +42 -0
- package/dist/event-view/AddToCalendarButton.svelte.d.ts +9 -0
- package/dist/event-view/EventBadges.svelte +20 -0
- package/dist/event-view/EventBadges.svelte.d.ts +7 -0
- package/dist/event-view/EventDateBlock.svelte +43 -0
- package/dist/event-view/EventDateBlock.svelte.d.ts +7 -0
- package/dist/event-view/EventHostedBy.svelte +63 -0
- package/dist/event-view/EventHostedBy.svelte.d.ts +16 -0
- package/dist/event-view/EventLinksList.svelte +37 -0
- package/dist/event-view/EventLinksList.svelte.d.ts +9 -0
- package/dist/event-view/EventLocationBlock.svelte +48 -0
- package/dist/event-view/EventLocationBlock.svelte.d.ts +7 -0
- package/dist/event-view/EventLocationMap.svelte +72 -0
- package/dist/event-view/EventLocationMap.svelte.d.ts +8 -0
- package/dist/event-view/ExternalRsvpNotice.svelte +44 -0
- package/dist/event-view/ExternalRsvpNotice.svelte.d.ts +6 -0
- package/dist/event-view/InviteShareFlow.svelte +177 -0
- package/dist/event-view/InviteShareFlow.svelte.d.ts +15 -0
- package/dist/event-view/StreamPlacePlayer.svelte +222 -0
- package/dist/event-view/StreamPlacePlayer.svelte.d.ts +8 -0
- package/dist/event-view/format.d.ts +26 -0
- package/dist/event-view/format.js +145 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/profile-url.d.ts +1 -0
- package/dist/profile-url.js +7 -0
- package/dist/theme.d.ts +9 -0
- package/dist/theme.js +22 -0
- package/dist/themes/Blobs.svelte +35 -0
- package/dist/themes/Blobs.svelte.d.ts +26 -0
- package/dist/themes/Butterflies.svelte +185 -0
- package/dist/themes/Butterflies.svelte.d.ts +3 -0
- package/dist/themes/Fireflies.svelte +134 -0
- package/dist/themes/Fireflies.svelte.d.ts +3 -0
- package/dist/themes/Kaleidoscope.svelte +177 -0
- package/dist/themes/Kaleidoscope.svelte.d.ts +3 -0
- package/dist/themes/Matrix.svelte +150 -0
- package/dist/themes/Matrix.svelte.d.ts +3 -0
- package/dist/themes/Stars.svelte +98 -0
- package/dist/themes/Stars.svelte.d.ts +3 -0
- package/dist/thumbnails/designs.d.ts +18 -0
- package/dist/thumbnails/designs.js +316 -0
- package/package.json +95 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Modal } from '@foxui/core';
|
|
3
|
+
import ThemePicker from '../ThemePicker.svelte';
|
|
4
|
+
import { themeBackgrounds, type EventTheme } from '../theme.js';
|
|
5
|
+
|
|
6
|
+
let { theme = $bindable() }: { theme: EventTheme } = $props();
|
|
7
|
+
|
|
8
|
+
let showModal = $state(false);
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div>
|
|
12
|
+
<p class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase">
|
|
13
|
+
Theme
|
|
14
|
+
</p>
|
|
15
|
+
<Button variant="secondary" size="sm" onclick={() => (showModal = true)}>
|
|
16
|
+
<svg
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
fill="none"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
stroke-width="1.5"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
class="size-4"
|
|
23
|
+
>
|
|
24
|
+
<path
|
|
25
|
+
stroke-linecap="round"
|
|
26
|
+
stroke-linejoin="round"
|
|
27
|
+
d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
{themeBackgrounds[theme.name] || theme.name}
|
|
31
|
+
</Button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<Modal bind:open={showModal}>
|
|
35
|
+
<p class="text-base-900 dark:text-base-50 text-lg font-semibold">Event theme</p>
|
|
36
|
+
<div class="mt-4">
|
|
37
|
+
<ThemePicker bind:theme />
|
|
38
|
+
</div>
|
|
39
|
+
</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,219 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Modal } from '@foxui/core';
|
|
3
|
+
import Avatar from 'svelte-boring-avatars';
|
|
4
|
+
import ThumbnailPresets from '../ThumbnailPresets.svelte';
|
|
5
|
+
import { designs, hashSeed, resolveAccentColor } from '../thumbnails/designs.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
rkey,
|
|
9
|
+
name,
|
|
10
|
+
dateStr,
|
|
11
|
+
accent,
|
|
12
|
+
thumbnailFile = $bindable(),
|
|
13
|
+
thumbnailPreview = $bindable(),
|
|
14
|
+
thumbnailChanged = $bindable(),
|
|
15
|
+
selectedPreset = $bindable()
|
|
16
|
+
}: {
|
|
17
|
+
rkey: string;
|
|
18
|
+
name: string;
|
|
19
|
+
dateStr: string;
|
|
20
|
+
accent: string;
|
|
21
|
+
thumbnailFile: File | null;
|
|
22
|
+
thumbnailPreview: string | null;
|
|
23
|
+
thumbnailChanged: boolean;
|
|
24
|
+
selectedPreset: string | null;
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
let fileInput: HTMLInputElement | undefined = $state();
|
|
28
|
+
let presetPreviewCanvas: HTMLCanvasElement | undefined = $state();
|
|
29
|
+
let showModal = $state(false);
|
|
30
|
+
let isDragOver = $state(false);
|
|
31
|
+
|
|
32
|
+
const seed = $derived(hashSeed(rkey));
|
|
33
|
+
|
|
34
|
+
function setThumbnail(file: File) {
|
|
35
|
+
thumbnailFile = file;
|
|
36
|
+
thumbnailChanged = true;
|
|
37
|
+
selectedPreset = null;
|
|
38
|
+
if (thumbnailPreview) URL.revokeObjectURL(thumbnailPreview);
|
|
39
|
+
thumbnailPreview = URL.createObjectURL(file);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function onFileChange(e: Event) {
|
|
43
|
+
const input = e.target as HTMLInputElement;
|
|
44
|
+
const file = input.files?.[0];
|
|
45
|
+
if (!file) return;
|
|
46
|
+
setThumbnail(file);
|
|
47
|
+
showModal = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onDragOver(e: DragEvent) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
isDragOver = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onDragLeave(e: DragEvent) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
isDragOver = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function onDrop(e: DragEvent) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
isDragOver = false;
|
|
63
|
+
const file = e.dataTransfer?.files?.[0];
|
|
64
|
+
if (file?.type.startsWith('image/')) {
|
|
65
|
+
setThumbnail(file);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function removeThumbnail() {
|
|
70
|
+
thumbnailFile = null;
|
|
71
|
+
thumbnailChanged = true;
|
|
72
|
+
selectedPreset = null;
|
|
73
|
+
if (thumbnailPreview) {
|
|
74
|
+
URL.revokeObjectURL(thumbnailPreview);
|
|
75
|
+
thumbnailPreview = null;
|
|
76
|
+
}
|
|
77
|
+
if (fileInput) fileInput.value = '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Render preset preview canvas whenever the selection, name, date, or accent changes.
|
|
81
|
+
$effect(() => {
|
|
82
|
+
if (selectedPreset && presetPreviewCanvas && designs[selectedPreset]) {
|
|
83
|
+
const ctx = presetPreviewCanvas.getContext('2d');
|
|
84
|
+
if (!ctx) return;
|
|
85
|
+
presetPreviewCanvas.width = 800;
|
|
86
|
+
presetPreviewCanvas.height = 800;
|
|
87
|
+
designs[selectedPreset](
|
|
88
|
+
ctx,
|
|
89
|
+
800,
|
|
90
|
+
800,
|
|
91
|
+
name || 'Event',
|
|
92
|
+
dateStr,
|
|
93
|
+
seed,
|
|
94
|
+
resolveAccentColor(accent)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
101
|
+
<div
|
|
102
|
+
class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none"
|
|
103
|
+
ondragover={onDragOver}
|
|
104
|
+
ondragleave={onDragLeave}
|
|
105
|
+
ondrop={onDrop}
|
|
106
|
+
>
|
|
107
|
+
<input
|
|
108
|
+
bind:this={fileInput}
|
|
109
|
+
type="file"
|
|
110
|
+
accept="image/*"
|
|
111
|
+
onchange={onFileChange}
|
|
112
|
+
class="hidden"
|
|
113
|
+
/>
|
|
114
|
+
<div class="group relative">
|
|
115
|
+
{#if thumbnailPreview}
|
|
116
|
+
<img
|
|
117
|
+
src={thumbnailPreview}
|
|
118
|
+
alt="Thumbnail preview"
|
|
119
|
+
class="border-base-200 dark:border-base-800 aspect-square w-full rounded-2xl border object-cover"
|
|
120
|
+
/>
|
|
121
|
+
{:else if selectedPreset && designs[selectedPreset]}
|
|
122
|
+
<div
|
|
123
|
+
class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border"
|
|
124
|
+
>
|
|
125
|
+
<canvas bind:this={presetPreviewCanvas} class="h-full w-full"></canvas>
|
|
126
|
+
</div>
|
|
127
|
+
{:else}
|
|
128
|
+
<div
|
|
129
|
+
class="bg-base-100 dark:bg-base-900 aspect-square w-full overflow-hidden rounded-2xl [&>svg]:h-full [&>svg]:w-full"
|
|
130
|
+
>
|
|
131
|
+
<Avatar
|
|
132
|
+
size={400}
|
|
133
|
+
name={rkey}
|
|
134
|
+
variant="marble"
|
|
135
|
+
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
|
|
136
|
+
square
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
{/if}
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onclick={() => (showModal = true)}
|
|
143
|
+
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
|
|
144
|
+
? 'bg-black/40 text-white/90'
|
|
145
|
+
: ''}"
|
|
146
|
+
>
|
|
147
|
+
<svg
|
|
148
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
149
|
+
fill="none"
|
|
150
|
+
viewBox="0 0 24 24"
|
|
151
|
+
stroke-width="1.5"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
class="size-6"
|
|
154
|
+
>
|
|
155
|
+
<path
|
|
156
|
+
stroke-linecap="round"
|
|
157
|
+
stroke-linejoin="round"
|
|
158
|
+
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"
|
|
159
|
+
/>
|
|
160
|
+
</svg>
|
|
161
|
+
<span class="text-sm font-medium">Change thumbnail</span>
|
|
162
|
+
</button>
|
|
163
|
+
{#if thumbnailPreview || selectedPreset}
|
|
164
|
+
<Button
|
|
165
|
+
variant="ghost"
|
|
166
|
+
size="iconSm"
|
|
167
|
+
onclick={removeThumbnail}
|
|
168
|
+
class="bg-base-900/70 absolute top-2 right-2 text-white opacity-0 transition-opacity group-hover:opacity-100 hover:bg-red-600"
|
|
169
|
+
>
|
|
170
|
+
<svg
|
|
171
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
172
|
+
viewBox="0 0 20 20"
|
|
173
|
+
fill="currentColor"
|
|
174
|
+
class="size-3.5"
|
|
175
|
+
>
|
|
176
|
+
<path
|
|
177
|
+
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"
|
|
178
|
+
/>
|
|
179
|
+
</svg>
|
|
180
|
+
</Button>
|
|
181
|
+
{/if}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<Modal bind:open={showModal}>
|
|
186
|
+
<p class="text-base-900 dark:text-base-50 text-lg font-semibold">Choose thumbnail</p>
|
|
187
|
+
<div class="mt-4 flex max-h-[70vh] flex-col gap-6 overflow-y-auto">
|
|
188
|
+
<Button variant="secondary" class="w-full" onclick={() => fileInput?.click()}>
|
|
189
|
+
<svg
|
|
190
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
191
|
+
fill="none"
|
|
192
|
+
viewBox="0 0 24 24"
|
|
193
|
+
stroke-width="1.5"
|
|
194
|
+
stroke="currentColor"
|
|
195
|
+
class="size-4"
|
|
196
|
+
>
|
|
197
|
+
<path
|
|
198
|
+
stroke-linecap="round"
|
|
199
|
+
stroke-linejoin="round"
|
|
200
|
+
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
Upload own thumbnail
|
|
204
|
+
</Button>
|
|
205
|
+
<ThumbnailPresets
|
|
206
|
+
{name}
|
|
207
|
+
{dateStr}
|
|
208
|
+
{accent}
|
|
209
|
+
{seed}
|
|
210
|
+
bind:selected={selectedPreset}
|
|
211
|
+
onselect={() => {
|
|
212
|
+
showModal = false;
|
|
213
|
+
thumbnailPreview = null;
|
|
214
|
+
thumbnailFile = null;
|
|
215
|
+
thumbnailChanged = true;
|
|
216
|
+
}}
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</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,98 @@
|
|
|
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 type EditorBlobRef = {
|
|
10
|
+
$type: 'blob';
|
|
11
|
+
ref: {
|
|
12
|
+
$link: string;
|
|
13
|
+
};
|
|
14
|
+
mimeType: string;
|
|
15
|
+
size: number;
|
|
16
|
+
};
|
|
17
|
+
export type EditorViewer = {
|
|
18
|
+
isLoggedIn: boolean;
|
|
19
|
+
did: string | null;
|
|
20
|
+
handle?: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
avatar?: string;
|
|
23
|
+
};
|
|
24
|
+
export type EditorAdapter = {
|
|
25
|
+
features: {
|
|
26
|
+
delete: boolean;
|
|
27
|
+
recurring: boolean;
|
|
28
|
+
privateMode: boolean;
|
|
29
|
+
};
|
|
30
|
+
putRecord(opts: {
|
|
31
|
+
collection: string;
|
|
32
|
+
rkey: string;
|
|
33
|
+
record: Record<string, unknown>;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
uri: string;
|
|
36
|
+
}>;
|
|
37
|
+
createRecord(opts: {
|
|
38
|
+
collection: string;
|
|
39
|
+
rkey?: string;
|
|
40
|
+
record: Record<string, unknown>;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
uri: string;
|
|
43
|
+
cid?: string;
|
|
44
|
+
}>;
|
|
45
|
+
deleteRecord(opts: {
|
|
46
|
+
collection: string;
|
|
47
|
+
rkey: string;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
uploadBlob(blob: Blob): Promise<EditorBlobRef>;
|
|
50
|
+
getRecord(opts: {
|
|
51
|
+
did: string;
|
|
52
|
+
collection: string;
|
|
53
|
+
rkey: string;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
value: Record<string, unknown>;
|
|
56
|
+
}>;
|
|
57
|
+
resolveHandle(handle: string): Promise<string>;
|
|
58
|
+
onSaved(result: {
|
|
59
|
+
uri: string;
|
|
60
|
+
rkey: string;
|
|
61
|
+
isNew: boolean;
|
|
62
|
+
spaceKey?: string;
|
|
63
|
+
}): void;
|
|
64
|
+
onDeleted?(): void;
|
|
65
|
+
requestLogin(): void;
|
|
66
|
+
notifyUpdate?(uri: string): Promise<void>;
|
|
67
|
+
createPrivateEvent?(opts: {
|
|
68
|
+
key: string;
|
|
69
|
+
record: Record<string, unknown>;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
spaceUri: string;
|
|
72
|
+
rkey: string;
|
|
73
|
+
spaceKey: string;
|
|
74
|
+
}>;
|
|
75
|
+
/** Put a record inside a permissioned space. Required for RSVPs to private events. */
|
|
76
|
+
putSpaceRecord?(opts: {
|
|
77
|
+
spaceUri: string;
|
|
78
|
+
collection: string;
|
|
79
|
+
rkey: string;
|
|
80
|
+
record: Record<string, unknown>;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
ok: boolean;
|
|
83
|
+
}>;
|
|
84
|
+
deleteSpaceRecord?(opts: {
|
|
85
|
+
spaceUri: string;
|
|
86
|
+
collection: string;
|
|
87
|
+
rkey: string;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
/** Mint an invite token for a private space. */
|
|
90
|
+
createSpaceInvite?(opts: {
|
|
91
|
+
spaceUri: string;
|
|
92
|
+
kind: 'read-join' | 'join';
|
|
93
|
+
maxUses?: number;
|
|
94
|
+
expiresAt?: number;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
token: string;
|
|
97
|
+
}>;
|
|
98
|
+
};
|
|
@@ -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,42 @@
|
|
|
1
|
+
import { type Token } from '@atcute/bluesky-richtext-parser';
|
|
2
|
+
import type { FlatEventRecord } from '../contrail.js';
|
|
3
|
+
import type { EventTheme } from '../theme.js';
|
|
4
|
+
import type { EventLocation, EventMode, Visibility } from './types.js';
|
|
5
|
+
export declare function tokensToFacets(tokens: Token[], resolveHandle: (handle: string) => Promise<string>): Promise<Record<string, unknown>[]>;
|
|
6
|
+
/** Render a selected thumbnail preset to a PNG File so it can be uploaded
|
|
7
|
+
* as a blob like a user-provided image. Returns null if the preset design
|
|
8
|
+
* is missing or the canvas fails to produce a blob. */
|
|
9
|
+
export declare function renderPresetThumbnail(args: {
|
|
10
|
+
design: string;
|
|
11
|
+
seed: number;
|
|
12
|
+
name: string;
|
|
13
|
+
dateStr: string;
|
|
14
|
+
accent?: string;
|
|
15
|
+
}): Promise<File | null>;
|
|
16
|
+
export declare function buildThumbnailMedia(args: {
|
|
17
|
+
isNew: boolean;
|
|
18
|
+
thumbnailChanged: boolean;
|
|
19
|
+
thumbnailFile: File | null;
|
|
20
|
+
existingMedia: Array<Record<string, unknown>>;
|
|
21
|
+
uploadBlob: (blob: Blob) => Promise<Record<string, unknown>>;
|
|
22
|
+
}): Promise<Array<Record<string, unknown>> | undefined>;
|
|
23
|
+
export declare function buildEventRecord(args: {
|
|
24
|
+
eventData: FlatEventRecord | null;
|
|
25
|
+
isNew: boolean;
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
startsAt: string;
|
|
29
|
+
endsAt: string;
|
|
30
|
+
timezone: string;
|
|
31
|
+
mode: EventMode;
|
|
32
|
+
visibility: Visibility;
|
|
33
|
+
theme: EventTheme;
|
|
34
|
+
links: Array<{
|
|
35
|
+
uri: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}>;
|
|
38
|
+
location: EventLocation | null;
|
|
39
|
+
locationChanged: boolean;
|
|
40
|
+
media: Array<Record<string, unknown>> | undefined;
|
|
41
|
+
resolveHandle: (handle: string) => Promise<string>;
|
|
42
|
+
}): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { compressImage } from '../atproto-helpers.js';
|
|
2
|
+
import { tokenize } from '@atcute/bluesky-richtext-parser';
|
|
3
|
+
import { datetimeLocalToISO } from '../date-format.js';
|
|
4
|
+
import { designs, resolveAccentColor } from '../thumbnails/designs.js';
|
|
5
|
+
export async function tokensToFacets(tokens, resolveHandle) {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const facets = [];
|
|
8
|
+
let byteOffset = 0;
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
const tokenBytes = encoder.encode(token.raw);
|
|
11
|
+
const byteStart = byteOffset;
|
|
12
|
+
const byteEnd = byteOffset + tokenBytes.length;
|
|
13
|
+
if (token.type === 'mention') {
|
|
14
|
+
try {
|
|
15
|
+
const did = await resolveHandle(token.handle);
|
|
16
|
+
if (did) {
|
|
17
|
+
facets.push({
|
|
18
|
+
index: { byteStart, byteEnd },
|
|
19
|
+
features: [{ $type: 'app.bsky.richtext.facet#mention', did }]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// skip unresolvable mentions
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (token.type === 'autolink') {
|
|
28
|
+
facets.push({
|
|
29
|
+
index: { byteStart, byteEnd },
|
|
30
|
+
features: [{ $type: 'app.bsky.richtext.facet#link', uri: token.url }]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (token.type === 'topic') {
|
|
34
|
+
facets.push({
|
|
35
|
+
index: { byteStart, byteEnd },
|
|
36
|
+
features: [{ $type: 'app.bsky.richtext.facet#tag', tag: token.name }]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
byteOffset = byteEnd;
|
|
40
|
+
}
|
|
41
|
+
return facets;
|
|
42
|
+
}
|
|
43
|
+
/** Render a selected thumbnail preset to a PNG File so it can be uploaded
|
|
44
|
+
* as a blob like a user-provided image. Returns null if the preset design
|
|
45
|
+
* is missing or the canvas fails to produce a blob. */
|
|
46
|
+
export async function renderPresetThumbnail(args) {
|
|
47
|
+
const drawer = designs[args.design];
|
|
48
|
+
if (!drawer)
|
|
49
|
+
return null;
|
|
50
|
+
const canvas = document.createElement('canvas');
|
|
51
|
+
canvas.width = 800;
|
|
52
|
+
canvas.height = 800;
|
|
53
|
+
const ctx = canvas.getContext('2d');
|
|
54
|
+
if (!ctx)
|
|
55
|
+
return null;
|
|
56
|
+
drawer(ctx, 800, 800, args.name.trim() || 'Event', args.dateStr, args.seed, resolveAccentColor(args.accent));
|
|
57
|
+
const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
|
|
58
|
+
if (!blob)
|
|
59
|
+
return null;
|
|
60
|
+
return new File([blob], 'thumbnail.png', { type: 'image/png' });
|
|
61
|
+
}
|
|
62
|
+
export async function buildThumbnailMedia(args) {
|
|
63
|
+
const { isNew, thumbnailChanged, thumbnailFile, existingMedia, uploadBlob } = args;
|
|
64
|
+
if (!isNew && !thumbnailChanged) {
|
|
65
|
+
return existingMedia.length > 0 ? existingMedia : undefined;
|
|
66
|
+
}
|
|
67
|
+
if (!thumbnailFile) {
|
|
68
|
+
const remaining = existingMedia.filter((m) => m.role !== 'thumbnail');
|
|
69
|
+
return remaining.length > 0 ? remaining : undefined;
|
|
70
|
+
}
|
|
71
|
+
const compressed = await compressImage(thumbnailFile);
|
|
72
|
+
const blobRef = await uploadBlob(compressed.blob);
|
|
73
|
+
if (!blobRef)
|
|
74
|
+
return existingMedia.length > 0 ? existingMedia : undefined;
|
|
75
|
+
return [
|
|
76
|
+
...existingMedia.filter((m) => m.role !== 'thumbnail'),
|
|
77
|
+
{
|
|
78
|
+
role: 'thumbnail',
|
|
79
|
+
content: blobRef,
|
|
80
|
+
aspect_ratio: {
|
|
81
|
+
width: compressed.aspectRatio.width,
|
|
82
|
+
height: compressed.aspectRatio.height
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
export async function buildEventRecord(args) {
|
|
88
|
+
const { eventData, isNew, name, description, startsAt, endsAt, timezone, mode, visibility, theme, links, location, locationChanged, media, resolveHandle } = args;
|
|
89
|
+
const createdAt = isNew
|
|
90
|
+
? new Date().toISOString()
|
|
91
|
+
: eventData?.createdAt || new Date().toISOString();
|
|
92
|
+
// Spread original record to preserve unspecced fields (e.g. additionalData)
|
|
93
|
+
const record = {
|
|
94
|
+
...(eventData ? { ...eventData } : {}),
|
|
95
|
+
$type: 'community.lexicon.calendar.event',
|
|
96
|
+
createdWith: 'https://atmo.rsvp',
|
|
97
|
+
name: name.trim(),
|
|
98
|
+
mode: `community.lexicon.calendar.event#${mode}`,
|
|
99
|
+
status: 'community.lexicon.calendar.event#scheduled',
|
|
100
|
+
startsAt: datetimeLocalToISO(startsAt, timezone),
|
|
101
|
+
timezone,
|
|
102
|
+
createdAt,
|
|
103
|
+
theme
|
|
104
|
+
};
|
|
105
|
+
// Remove flattened fields that aren't part of the actual record
|
|
106
|
+
for (const k of [
|
|
107
|
+
'cid',
|
|
108
|
+
'did',
|
|
109
|
+
'rkey',
|
|
110
|
+
'uri',
|
|
111
|
+
'rsvps',
|
|
112
|
+
'rsvpsCount',
|
|
113
|
+
'rsvpsGoingCount',
|
|
114
|
+
'rsvpsInterestedCount',
|
|
115
|
+
'rsvpsNotgoingCount'
|
|
116
|
+
]) {
|
|
117
|
+
delete record[k];
|
|
118
|
+
}
|
|
119
|
+
const trimmedDescription = description.trim();
|
|
120
|
+
if (trimmedDescription) {
|
|
121
|
+
record.description = trimmedDescription;
|
|
122
|
+
const tokens = tokenize(trimmedDescription);
|
|
123
|
+
const facets = await tokensToFacets(tokens, resolveHandle);
|
|
124
|
+
if (facets.length > 0)
|
|
125
|
+
record.facets = facets;
|
|
126
|
+
}
|
|
127
|
+
if (endsAt)
|
|
128
|
+
record.endsAt = datetimeLocalToISO(endsAt, timezone);
|
|
129
|
+
if (media)
|
|
130
|
+
record.media = media;
|
|
131
|
+
if (links.length > 0)
|
|
132
|
+
record.uris = links;
|
|
133
|
+
if (isNew || locationChanged) {
|
|
134
|
+
if (location) {
|
|
135
|
+
record.locations = [
|
|
136
|
+
{
|
|
137
|
+
$type: 'community.lexicon.location.address',
|
|
138
|
+
...location
|
|
139
|
+
}
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
// If changed/new but no location, locations stays undefined (removed/absent)
|
|
143
|
+
}
|
|
144
|
+
else if (eventData?.locations && eventData.locations.length > 0) {
|
|
145
|
+
record.locations = eventData.locations;
|
|
146
|
+
}
|
|
147
|
+
const existingPrefs = (record.preferences ??
|
|
148
|
+
{});
|
|
149
|
+
record.preferences = {
|
|
150
|
+
...existingPrefs,
|
|
151
|
+
showInDiscovery: visibility !== 'unlisted'
|
|
152
|
+
};
|
|
153
|
+
return record;
|
|
154
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type EventMode = 'inperson' | 'virtual' | 'hybrid';
|
|
2
|
+
export type Visibility = 'public' | 'private' | 'unlisted';
|
|
3
|
+
export interface EventLocation {
|
|
4
|
+
street?: string;
|
|
5
|
+
locality?: string;
|
|
6
|
+
region?: string;
|
|
7
|
+
country?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Optional autofill payload for a brand-new event. EventEditor populates its
|
|
11
|
+
* fields from this on mount (only when there is no `eventData`), while leaving
|
|
12
|
+
* `isNew` true so the save path still treats the result as a creation. Use
|
|
13
|
+
* `additionalData` to carry atmo-specific extras (e.g. an external source link
|
|
14
|
+
* + rsvp mode) into the saved record.
|
|
15
|
+
*/
|
|
16
|
+
export interface EventEditorPrefill {
|
|
17
|
+
name?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** ISO 8601 string. */
|
|
20
|
+
startsAt?: string;
|
|
21
|
+
/** ISO 8601 string. */
|
|
22
|
+
endsAt?: string;
|
|
23
|
+
timezone?: string;
|
|
24
|
+
mode?: EventMode;
|
|
25
|
+
location?: EventLocation;
|
|
26
|
+
links?: Array<{
|
|
27
|
+
uri: string;
|
|
28
|
+
name: string;
|
|
29
|
+
}>;
|
|
30
|
+
additionalData?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Pre-supplied cover image. When set, the editor uses this instead of
|
|
33
|
+
* auto-generating a preset thumbnail and the file is uploaded as a blob on
|
|
34
|
+
* save.
|
|
35
|
+
*/
|
|
36
|
+
thumbnailFile?: File;
|
|
37
|
+
}
|
|
38
|
+
export declare function stripModePrefix(modeStr: string): EventMode;
|
|
39
|
+
export declare function getLocationDisplayString(loc: EventLocation): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function stripModePrefix(modeStr) {
|
|
2
|
+
const stripped = modeStr.replace('community.lexicon.calendar.event#', '');
|
|
3
|
+
if (stripped === 'virtual' || stripped === 'hybrid' || stripped === 'inperson')
|
|
4
|
+
return stripped;
|
|
5
|
+
return 'inperson';
|
|
6
|
+
}
|
|
7
|
+
export function getLocationDisplayString(loc) {
|
|
8
|
+
return [loc.street, loc.locality, loc.region, loc.country].filter(Boolean).join(', ');
|
|
9
|
+
}
|