@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,237 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as TID from '@atcute/tid';
|
|
3
|
+
import { Avatar, Button } from '@foxui/core';
|
|
4
|
+
import { launchConfetti } from '@foxui/visual';
|
|
5
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
eventUri,
|
|
9
|
+
eventCid,
|
|
10
|
+
initialRsvpStatus = null,
|
|
11
|
+
initialRsvpRkey = null,
|
|
12
|
+
spaceUri = null,
|
|
13
|
+
adapter,
|
|
14
|
+
viewer,
|
|
15
|
+
onrsvp,
|
|
16
|
+
oncancel,
|
|
17
|
+
onlogin
|
|
18
|
+
}: {
|
|
19
|
+
eventUri: string;
|
|
20
|
+
eventCid: string | null;
|
|
21
|
+
initialRsvpStatus?: 'going' | 'interested' | 'notgoing' | null;
|
|
22
|
+
initialRsvpRkey?: string | null;
|
|
23
|
+
/** If set, RSVPs write into this space instead of the user's public PDS. */
|
|
24
|
+
spaceUri?: string | null;
|
|
25
|
+
adapter: EditorAdapter;
|
|
26
|
+
viewer: EditorViewer;
|
|
27
|
+
onrsvp?: (status: 'going' | 'interested', rkey: string) => void;
|
|
28
|
+
oncancel?: () => void;
|
|
29
|
+
onlogin?: () => void;
|
|
30
|
+
} = $props();
|
|
31
|
+
|
|
32
|
+
let rsvpStatusOverride: 'going' | 'interested' | 'notgoing' | null | undefined = $state(
|
|
33
|
+
undefined
|
|
34
|
+
);
|
|
35
|
+
let rsvpRkeyOverride: string | null | undefined = $state(undefined);
|
|
36
|
+
let rsvpSubmitting = $state(false);
|
|
37
|
+
|
|
38
|
+
let rsvpStatus = $derived(rsvpStatusOverride !== undefined ? rsvpStatusOverride : initialRsvpStatus);
|
|
39
|
+
let rsvpRkey = $derived(rsvpRkeyOverride !== undefined ? rsvpRkeyOverride : initialRsvpRkey);
|
|
40
|
+
|
|
41
|
+
async function submitRsvp(status: 'going' | 'interested') {
|
|
42
|
+
if (!viewer.isLoggedIn || !viewer.did) return;
|
|
43
|
+
rsvpSubmitting = true;
|
|
44
|
+
try {
|
|
45
|
+
const key = rsvpRkey ?? TID.now();
|
|
46
|
+
const record = {
|
|
47
|
+
$type: 'community.lexicon.calendar.rsvp',
|
|
48
|
+
createdWith: 'https://atmo.rsvp',
|
|
49
|
+
status: `community.lexicon.calendar.rsvp#${status}`,
|
|
50
|
+
subject: {
|
|
51
|
+
uri: eventUri,
|
|
52
|
+
...(eventCid ? { cid: eventCid } : {})
|
|
53
|
+
},
|
|
54
|
+
createdAt: new Date().toISOString()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let ok = false;
|
|
58
|
+
if (spaceUri) {
|
|
59
|
+
if (!adapter.putSpaceRecord) {
|
|
60
|
+
console.error('putSpaceRecord not supported by this adapter');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const result = await adapter.putSpaceRecord({
|
|
64
|
+
spaceUri,
|
|
65
|
+
collection: 'community.lexicon.calendar.rsvp',
|
|
66
|
+
rkey: key,
|
|
67
|
+
record
|
|
68
|
+
});
|
|
69
|
+
ok = result.ok;
|
|
70
|
+
} else {
|
|
71
|
+
try {
|
|
72
|
+
await adapter.putRecord({
|
|
73
|
+
collection: 'community.lexicon.calendar.rsvp',
|
|
74
|
+
rkey: key,
|
|
75
|
+
record
|
|
76
|
+
});
|
|
77
|
+
ok = true;
|
|
78
|
+
adapter.notifyUpdate?.(
|
|
79
|
+
`at://${viewer.did}/community.lexicon.calendar.rsvp/${key}`
|
|
80
|
+
);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.error('RSVP putRecord failed:', e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ok) {
|
|
87
|
+
rsvpStatusOverride = status;
|
|
88
|
+
rsvpRkeyOverride = key;
|
|
89
|
+
launchConfetti();
|
|
90
|
+
onrsvp?.(status, key);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error('Failed to submit RSVP:', e);
|
|
94
|
+
} finally {
|
|
95
|
+
rsvpSubmitting = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function cancelRsvp() {
|
|
100
|
+
if (!viewer.isLoggedIn || !viewer.did || !rsvpRkey) return;
|
|
101
|
+
rsvpSubmitting = true;
|
|
102
|
+
try {
|
|
103
|
+
if (spaceUri) {
|
|
104
|
+
if (!adapter.deleteSpaceRecord) {
|
|
105
|
+
console.error('deleteSpaceRecord not supported by this adapter');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await adapter.deleteSpaceRecord({
|
|
109
|
+
spaceUri,
|
|
110
|
+
collection: 'community.lexicon.calendar.rsvp',
|
|
111
|
+
rkey: rsvpRkey
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
await adapter.deleteRecord({
|
|
115
|
+
collection: 'community.lexicon.calendar.rsvp',
|
|
116
|
+
rkey: rsvpRkey
|
|
117
|
+
});
|
|
118
|
+
adapter.notifyUpdate?.(
|
|
119
|
+
`at://${viewer.did}/community.lexicon.calendar.rsvp/${rsvpRkey}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
rsvpStatusOverride = null;
|
|
123
|
+
rsvpRkeyOverride = null;
|
|
124
|
+
oncancel?.();
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error('Failed to cancel RSVP:', e);
|
|
127
|
+
} finally {
|
|
128
|
+
rsvpSubmitting = false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
class="border-base-200 dark:border-base-800 bg-base-100 items-between dark:bg-base-950/50 mt-8 mb-2 flex h-25 flex-col justify-center rounded-2xl border p-4"
|
|
135
|
+
>
|
|
136
|
+
{#if !viewer.isLoggedIn}
|
|
137
|
+
<div class="flex items-center justify-between gap-4">
|
|
138
|
+
<p class="text-base-600 dark:text-base-400 text-sm">Log in to RSVP to this event</p>
|
|
139
|
+
|
|
140
|
+
<Button onclick={() => { onlogin?.(); adapter.requestLogin(); }}>Log in to RSVP</Button>
|
|
141
|
+
</div>
|
|
142
|
+
{:else if rsvpStatus === 'going'}
|
|
143
|
+
<div class="flex items-center justify-between">
|
|
144
|
+
<div class="flex items-center gap-3">
|
|
145
|
+
<div
|
|
146
|
+
class="flex size-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30"
|
|
147
|
+
>
|
|
148
|
+
<svg
|
|
149
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
150
|
+
viewBox="0 0 20 20"
|
|
151
|
+
fill="currentColor"
|
|
152
|
+
class="size-4 text-green-600 dark:text-green-400"
|
|
153
|
+
>
|
|
154
|
+
<path
|
|
155
|
+
fill-rule="evenodd"
|
|
156
|
+
d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z"
|
|
157
|
+
clip-rule="evenodd"
|
|
158
|
+
/>
|
|
159
|
+
</svg>
|
|
160
|
+
</div>
|
|
161
|
+
<p class="text-base-900 dark:text-base-50 font-semibold">You're Going</p>
|
|
162
|
+
</div>
|
|
163
|
+
<Button onclick={cancelRsvp} disabled={rsvpSubmitting} variant="ghost">Remove</Button>
|
|
164
|
+
</div>
|
|
165
|
+
{:else if rsvpStatus === 'interested'}
|
|
166
|
+
<div class="flex items-center justify-between">
|
|
167
|
+
<div class="flex items-center gap-3">
|
|
168
|
+
<div
|
|
169
|
+
class="flex size-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30"
|
|
170
|
+
>
|
|
171
|
+
<svg
|
|
172
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
173
|
+
viewBox="0 0 20 20"
|
|
174
|
+
fill="currentColor"
|
|
175
|
+
class="size-4 text-amber-600 dark:text-amber-400"
|
|
176
|
+
>
|
|
177
|
+
<path
|
|
178
|
+
fill-rule="evenodd"
|
|
179
|
+
d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401Z"
|
|
180
|
+
clip-rule="evenodd"
|
|
181
|
+
/>
|
|
182
|
+
</svg>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="text-base-900 dark:text-base-50 font-semibold">You're Interested</p>
|
|
185
|
+
</div>
|
|
186
|
+
<Button onclick={cancelRsvp} disabled={rsvpSubmitting} variant="ghost">Remove</Button>
|
|
187
|
+
</div>
|
|
188
|
+
{:else if rsvpStatus === 'notgoing'}
|
|
189
|
+
<div class="flex items-center justify-between">
|
|
190
|
+
<div class="flex items-center gap-3">
|
|
191
|
+
<div
|
|
192
|
+
class="flex size-8 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30"
|
|
193
|
+
>
|
|
194
|
+
<svg
|
|
195
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
196
|
+
viewBox="0 0 20 20"
|
|
197
|
+
fill="currentColor"
|
|
198
|
+
class="size-4 text-red-600 dark:text-red-400"
|
|
199
|
+
>
|
|
200
|
+
<path
|
|
201
|
+
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"
|
|
202
|
+
/>
|
|
203
|
+
</svg>
|
|
204
|
+
</div>
|
|
205
|
+
<p class="text-base-900 dark:text-base-50 font-semibold">Not Going</p>
|
|
206
|
+
</div>
|
|
207
|
+
<Button onclick={cancelRsvp} disabled={rsvpSubmitting} variant="ghost">Remove</Button>
|
|
208
|
+
</div>
|
|
209
|
+
{:else}
|
|
210
|
+
{#if viewer.isLoggedIn}
|
|
211
|
+
<div class="mb-4 flex items-center gap-2">
|
|
212
|
+
<span class="text-base-500 dark:text-base-400 text-sm">Attend as</span>
|
|
213
|
+
<Avatar
|
|
214
|
+
src={viewer.avatar}
|
|
215
|
+
alt={viewer.displayName || viewer.handle || viewer.did || ''}
|
|
216
|
+
class="size-5"
|
|
217
|
+
/>
|
|
218
|
+
<span class="text-base-700 dark:text-base-300 truncate text-sm font-medium">
|
|
219
|
+
{viewer.displayName || viewer.handle || viewer.did}
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
{/if}
|
|
223
|
+
<div class="flex gap-3">
|
|
224
|
+
<Button onclick={() => submitRsvp('going')} disabled={rsvpSubmitting} class="flex-1">
|
|
225
|
+
{rsvpSubmitting ? '...' : 'Going'}
|
|
226
|
+
</Button>
|
|
227
|
+
<Button
|
|
228
|
+
onclick={() => submitRsvp('interested')}
|
|
229
|
+
disabled={rsvpSubmitting}
|
|
230
|
+
variant="secondary"
|
|
231
|
+
class="flex-1"
|
|
232
|
+
>
|
|
233
|
+
{rsvpSubmitting ? '...' : 'Interested'}
|
|
234
|
+
</Button>
|
|
235
|
+
</div>
|
|
236
|
+
{/if}
|
|
237
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
eventUri: string;
|
|
4
|
+
eventCid: string | null;
|
|
5
|
+
initialRsvpStatus?: 'going' | 'interested' | 'notgoing' | null;
|
|
6
|
+
initialRsvpRkey?: string | null;
|
|
7
|
+
/** If set, RSVPs write into this space instead of the user's public PDS. */
|
|
8
|
+
spaceUri?: string | null;
|
|
9
|
+
adapter: EditorAdapter;
|
|
10
|
+
viewer: EditorViewer;
|
|
11
|
+
onrsvp?: (status: 'going' | 'interested', rkey: string) => void;
|
|
12
|
+
oncancel?: () => void;
|
|
13
|
+
onlogin?: () => void;
|
|
14
|
+
};
|
|
15
|
+
declare const EventRsvp: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type EventRsvp = ReturnType<typeof EventRsvp>;
|
|
17
|
+
export default EventRsvp;
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { eventUrl, isEventOngoing, type FlatEventRecord } from './contrail.js';
|
|
3
|
+
import { getCDNImageBlobUrl } from './atproto-helpers.js';
|
|
4
|
+
import { Button, ToggleGroup, ToggleGroupItem } from '@foxui/core';
|
|
5
|
+
import ShareModal from './ShareModal.svelte';
|
|
6
|
+
import EventComments from './EventComments.svelte';
|
|
7
|
+
import Avatar from 'svelte-boring-avatars';
|
|
8
|
+
import EventRsvp from './EventRsvp.svelte';
|
|
9
|
+
import EventCard from './EventCard.svelte';
|
|
10
|
+
import EventAttendees from './EventAttendees.svelte';
|
|
11
|
+
import VodPlayer, { type VodPlayerApi } from './VodPlayer.svelte';
|
|
12
|
+
import StreamPlacePlayer from './event-view/StreamPlacePlayer.svelte';
|
|
13
|
+
import { launchConfetti } from '@foxui/visual';
|
|
14
|
+
import ThemeBackground from './ThemeBackground.svelte';
|
|
15
|
+
import ThemeApply from './ThemeApply.svelte';
|
|
16
|
+
import { defaultTheme, type EventTheme } from './theme.js';
|
|
17
|
+
import { onMount } from 'svelte';
|
|
18
|
+
import type { EditorAdapter, EditorViewer } from './editor/adapter.js';
|
|
19
|
+
|
|
20
|
+
import EventBadges from './event-view/EventBadges.svelte';
|
|
21
|
+
import EventDateBlock from './event-view/EventDateBlock.svelte';
|
|
22
|
+
import EventLocationBlock from './event-view/EventLocationBlock.svelte';
|
|
23
|
+
import EventLocationMap from './event-view/EventLocationMap.svelte';
|
|
24
|
+
import EventHostedBy from './event-view/EventHostedBy.svelte';
|
|
25
|
+
import EventLinksList from './event-view/EventLinksList.svelte';
|
|
26
|
+
import AddToCalendarButton from './event-view/AddToCalendarButton.svelte';
|
|
27
|
+
import InviteShareFlow from './event-view/InviteShareFlow.svelte';
|
|
28
|
+
import ExternalRsvpNotice from './event-view/ExternalRsvpNotice.svelte';
|
|
29
|
+
import { buildDescriptionHtml, getLocationData, resolveGeoLocation, type GeoLocation } from './event-view/format';
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
data,
|
|
33
|
+
adapter,
|
|
34
|
+
viewer,
|
|
35
|
+
pageUrl,
|
|
36
|
+
embedMode = false,
|
|
37
|
+
shareUrlOverride
|
|
38
|
+
}: {
|
|
39
|
+
data: any;
|
|
40
|
+
adapter: EditorAdapter;
|
|
41
|
+
viewer: EditorViewer;
|
|
42
|
+
/** Current page URL — used for the OG image link and the calendar button. */
|
|
43
|
+
pageUrl: URL;
|
|
44
|
+
embedMode?: boolean;
|
|
45
|
+
/** When set, the share modal / Bluesky post embed use this URL instead
|
|
46
|
+
* of the canonical atmo.rsvp event URL. Useful for embedders that want
|
|
47
|
+
* share links to point at their own event page. */
|
|
48
|
+
shareUrlOverride?: string;
|
|
49
|
+
} = $props();
|
|
50
|
+
|
|
51
|
+
let eventData: FlatEventRecord = $derived(data.eventData);
|
|
52
|
+
let did: string = $derived(data.actorDid);
|
|
53
|
+
let rkey: string = $derived(data.rkey);
|
|
54
|
+
let hostProfile = $derived(data.hostProfile);
|
|
55
|
+
let attendees = $derived(data.attendees);
|
|
56
|
+
|
|
57
|
+
let theme: EventTheme = $derived(eventData.theme ?? defaultTheme);
|
|
58
|
+
|
|
59
|
+
let hostUrl = $derived(`/p/${hostProfile?.handle || did}`);
|
|
60
|
+
let eventPath = $derived(eventUrl(eventData, hostProfile?.handle || did));
|
|
61
|
+
let shareUrl = $derived(
|
|
62
|
+
shareUrlOverride
|
|
63
|
+
? shareUrlOverride
|
|
64
|
+
: typeof window !== 'undefined'
|
|
65
|
+
? `${window.location.origin}${eventPath}`
|
|
66
|
+
: eventPath
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Times are always rendered in the viewer's local timezone — the stored UTC
|
|
70
|
+
// instant is what the Date constructor parses, and toLocaleString/Time uses
|
|
71
|
+
// the browser's zone by default.
|
|
72
|
+
let startDate = $derived(new Date(eventData.startsAt));
|
|
73
|
+
let endDate = $derived(eventData.endsAt ? new Date(eventData.endsAt) : null);
|
|
74
|
+
|
|
75
|
+
let locationData = $derived(getLocationData(eventData.locations));
|
|
76
|
+
let geoLocation: GeoLocation | null = $state(null);
|
|
77
|
+
|
|
78
|
+
let showShareModal = $state(false);
|
|
79
|
+
let shareModalTitle = $state('Event created!');
|
|
80
|
+
let shareModalText: string | undefined = $state(undefined);
|
|
81
|
+
// True only when the share modal was opened via the post-creation flow by
|
|
82
|
+
// the event's host. Drives the "show comments on event page" checkbox and
|
|
83
|
+
// the bskyPostRef write — RSVP shares should never overwrite the comments
|
|
84
|
+
// root, even when the RSVPer is the host.
|
|
85
|
+
let canSetEventComments = $state(false);
|
|
86
|
+
let isHost = $derived(!!viewer.did && viewer.did === did);
|
|
87
|
+
let hasComments = $derived(
|
|
88
|
+
!!eventData.bskyPostRef?.showComments && !!eventData.bskyPostRef?.uri
|
|
89
|
+
);
|
|
90
|
+
let aboutCommentsTab = $state<'about' | 'comments'>('about');
|
|
91
|
+
|
|
92
|
+
onMount(async () => {
|
|
93
|
+
geoLocation = await resolveGeoLocation(eventData.locations, locationData);
|
|
94
|
+
|
|
95
|
+
const url = new URL(window.location.href);
|
|
96
|
+
if (url.searchParams.has('created')) {
|
|
97
|
+
url.searchParams.delete('created');
|
|
98
|
+
history.replaceState({}, '', url.pathname);
|
|
99
|
+
launchConfetti();
|
|
100
|
+
shareModalTitle = 'Event created!';
|
|
101
|
+
shareModalText = `I'm hosting "${eventData.name}"!\n\n${shareUrl}`;
|
|
102
|
+
canSetEventComments = isHost;
|
|
103
|
+
showShareModal = true;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let thumbnailImage = $derived.by(() => {
|
|
108
|
+
if (!eventData.media || eventData.media.length === 0) return null;
|
|
109
|
+
const media = eventData.media.find((m) => m.role === 'thumbnail');
|
|
110
|
+
if (!media?.content) return null;
|
|
111
|
+
const url = getCDNImageBlobUrl({ did, blob: media.content });
|
|
112
|
+
if (!url) return null;
|
|
113
|
+
return { url, alt: media.alt || eventData.name };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let bannerImage = $derived.by(() => {
|
|
117
|
+
if (!eventData.media || eventData.media.length === 0) return null;
|
|
118
|
+
const media = eventData.media.find((m) => m.role === 'header');
|
|
119
|
+
if (!media?.content) return null;
|
|
120
|
+
const url = getCDNImageBlobUrl({ did, blob: media.content });
|
|
121
|
+
if (!url) return null;
|
|
122
|
+
return { url, alt: media.alt || eventData.name };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Prefer thumbnail; fall back to header/banner image
|
|
126
|
+
let displayImage = $derived(thumbnailImage ?? bannerImage);
|
|
127
|
+
let isBannerOnly = $derived(!thumbnailImage && !!bannerImage);
|
|
128
|
+
|
|
129
|
+
let isOngoing = $derived(isEventOngoing(eventData.startsAt, eventData.endsAt));
|
|
130
|
+
let isPast = $derived(endDate ? endDate < new Date() : false);
|
|
131
|
+
|
|
132
|
+
let streamPlaceHandle = $derived.by(() => {
|
|
133
|
+
const uris = eventData.uris;
|
|
134
|
+
if (!uris) return null;
|
|
135
|
+
for (const { uri } of uris) {
|
|
136
|
+
const m = uri.match(/^https?:\/\/stream\.place\/([^/?#]+)/i);
|
|
137
|
+
if (m) return m[1];
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
let descriptionHtml = $derived(
|
|
143
|
+
buildDescriptionHtml(eventData.description, eventData.facets)
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`);
|
|
147
|
+
|
|
148
|
+
let ogImageUrl = $derived(data.ogImage ?? `${pageUrl.origin}${pageUrl.pathname}/og.png`);
|
|
149
|
+
|
|
150
|
+
let isOwner = $derived(!embedMode && viewer.isLoggedIn && viewer.did === did);
|
|
151
|
+
|
|
152
|
+
let speakers = $derived(data.speakerProfiles ?? []);
|
|
153
|
+
|
|
154
|
+
// Imported events can opt out of atmo's own RSVPs (rsvpMode === 'external_only').
|
|
155
|
+
// In that case we hide the RSVP controls and link out to the original event page.
|
|
156
|
+
let externalSource = $derived(
|
|
157
|
+
(eventData.additionalData as Record<string, unknown> | undefined)?.externalSource as
|
|
158
|
+
| { url?: string; rsvpMode?: 'external_only' | 'atmo_too' }
|
|
159
|
+
| undefined
|
|
160
|
+
);
|
|
161
|
+
let rsvpExternalOnly = $derived(
|
|
162
|
+
externalSource?.rsvpMode === 'external_only' && !!externalSource?.url
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
let vodCurrentTime = $state(0);
|
|
166
|
+
let vodApi: VodPlayerApi | undefined = $state();
|
|
167
|
+
|
|
168
|
+
let attendeesRef: EventAttendees | undefined = $state();
|
|
169
|
+
|
|
170
|
+
function handleRsvp(status: 'going' | 'interested') {
|
|
171
|
+
if (!viewer.did) return;
|
|
172
|
+
attendeesRef?.addAttendee({
|
|
173
|
+
did: viewer.did,
|
|
174
|
+
status,
|
|
175
|
+
avatar: viewer.avatar,
|
|
176
|
+
name: viewer.displayName || viewer.handle || viewer.did,
|
|
177
|
+
handle: viewer.handle,
|
|
178
|
+
url: `/${viewer.handle || viewer.did}`
|
|
179
|
+
});
|
|
180
|
+
if (status === 'interested') return;
|
|
181
|
+
shareModalTitle = "You're going!";
|
|
182
|
+
shareModalText = `I'm going to "${eventData.name}".\n\n${shareUrl}`;
|
|
183
|
+
canSetEventComments = false;
|
|
184
|
+
showShareModal = true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function handleRsvpCancel() {
|
|
188
|
+
if (!viewer.did) return;
|
|
189
|
+
attendeesRef?.removeAttendee(viewer.did);
|
|
190
|
+
}
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<svelte:head>
|
|
194
|
+
<title>{eventData.name}</title>
|
|
195
|
+
<meta name="description" content={eventData.description || `Event: ${eventData.name}`} />
|
|
196
|
+
<meta property="og:title" content={eventData.name} />
|
|
197
|
+
<meta property="og:description" content={eventData.description || `Event: ${eventData.name}`} />
|
|
198
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
199
|
+
<meta name="twitter:title" content={eventData.name} />
|
|
200
|
+
<meta name="twitter:description" content={eventData.description || `Event: ${eventData.name}`} />
|
|
201
|
+
<meta name="twitter:image" content={ogImageUrl} />
|
|
202
|
+
</svelte:head>
|
|
203
|
+
|
|
204
|
+
<ThemeApply accentColor={theme.accentColor} baseColor={theme.baseColor} />
|
|
205
|
+
<ThemeBackground {theme} />
|
|
206
|
+
|
|
207
|
+
<div class="min-h-screen px-6 py-12 sm:py-12">
|
|
208
|
+
<div class="mx-auto max-w-3xl">
|
|
209
|
+
<!-- Banner image (full width, only when no thumbnail) -->
|
|
210
|
+
{#if isBannerOnly && displayImage}
|
|
211
|
+
<img
|
|
212
|
+
src={displayImage.url}
|
|
213
|
+
alt={displayImage.alt}
|
|
214
|
+
class="border-base-200 dark:border-base-800 mb-8 aspect-3/1 w-full rounded-2xl border object-cover"
|
|
215
|
+
/>
|
|
216
|
+
{/if}
|
|
217
|
+
|
|
218
|
+
<!-- Two-column layout: image left, details right -->
|
|
219
|
+
<div
|
|
220
|
+
class="grid grid-cols-1 gap-8 md:grid-cols-[14rem_1fr] md:grid-rows-[auto_1fr] md:gap-x-10 md:gap-y-6 lg:grid-cols-[16rem_1fr]"
|
|
221
|
+
>
|
|
222
|
+
<!-- Thumbnail image (left column) -->
|
|
223
|
+
{#if !isBannerOnly}
|
|
224
|
+
<div class="order-1 max-w-sm md:order-0 md:col-start-1 md:max-w-none">
|
|
225
|
+
{#if displayImage}
|
|
226
|
+
<img
|
|
227
|
+
src={displayImage.url}
|
|
228
|
+
alt={displayImage.alt}
|
|
229
|
+
class="border-base-200 dark:border-base-800 bg-base-200 dark:bg-base-950/50 aspect-square w-full rounded-2xl border object-cover"
|
|
230
|
+
/>
|
|
231
|
+
{:else}
|
|
232
|
+
<div
|
|
233
|
+
class="border-base-200 dark:border-base-800 aspect-square w-full overflow-hidden rounded-2xl border [&>svg]:h-full [&>svg]:w-full"
|
|
234
|
+
>
|
|
235
|
+
<Avatar
|
|
236
|
+
size={256}
|
|
237
|
+
name={data.rkey}
|
|
238
|
+
variant="marble"
|
|
239
|
+
colors={['#92A1C6', '#146A7C', '#F0AB3D', '#C271B4', '#C20D90']}
|
|
240
|
+
square
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
{/if}
|
|
244
|
+
{#if isOwner}
|
|
245
|
+
<Button href="./{rkey}/edit" class="mt-9 w-full">Edit Event</Button>
|
|
246
|
+
{#if data.spaceUri}
|
|
247
|
+
<InviteShareFlow
|
|
248
|
+
spaceUri={data.spaceUri}
|
|
249
|
+
spaceKey={data.spaceKey}
|
|
250
|
+
{did}
|
|
251
|
+
{rkey}
|
|
252
|
+
eventName={eventData.name}
|
|
253
|
+
{hostProfile}
|
|
254
|
+
{adapter}
|
|
255
|
+
{viewer}
|
|
256
|
+
/>
|
|
257
|
+
{/if}
|
|
258
|
+
{/if}
|
|
259
|
+
</div>
|
|
260
|
+
{/if}
|
|
261
|
+
|
|
262
|
+
<!-- Right column: event details -->
|
|
263
|
+
<div class="order-2 min-w-0 md:order-0 md:col-start-2 md:row-span-2 md:row-start-1">
|
|
264
|
+
<div class="mb-2">
|
|
265
|
+
<h1 class="text-base-900 dark:text-base-50 text-3xl leading-tight font-bold sm:text-4xl">
|
|
266
|
+
{eventData.name}
|
|
267
|
+
</h1>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<EventBadges mode={eventData.mode} {isOngoing} />
|
|
271
|
+
|
|
272
|
+
<EventDateBlock {startDate} {endDate} />
|
|
273
|
+
|
|
274
|
+
<EventLocationBlock {locationData} />
|
|
275
|
+
|
|
276
|
+
<!-- Part of -->
|
|
277
|
+
{#if data.parentEvent}
|
|
278
|
+
<div
|
|
279
|
+
class="border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-950/50 mt-8 mb-2 justify-center rounded-2xl border p-4"
|
|
280
|
+
>
|
|
281
|
+
<p
|
|
282
|
+
class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase"
|
|
283
|
+
>
|
|
284
|
+
Part of
|
|
285
|
+
</p>
|
|
286
|
+
<EventCard event={data.parentEvent} actor="atprotocol.dev" />
|
|
287
|
+
<Button href="/p/atmosphereconf.org" size="lg" class="mt-6 w-full">
|
|
288
|
+
See full schedule
|
|
289
|
+
</Button>
|
|
290
|
+
</div>
|
|
291
|
+
{/if}
|
|
292
|
+
|
|
293
|
+
{#if did === 'did:plc:lehcqqkwzcwvjvw66uthu5oq' && rkey === '3lte3c7x43l2e'}
|
|
294
|
+
<Button href="/p/atmosphereconf.org" size="lg" class="mb-4 w-full">
|
|
295
|
+
See full schedule
|
|
296
|
+
</Button>
|
|
297
|
+
{/if}
|
|
298
|
+
|
|
299
|
+
{#if !isPast}
|
|
300
|
+
{#if rsvpExternalOnly && externalSource?.url}
|
|
301
|
+
<ExternalRsvpNotice url={externalSource.url} />
|
|
302
|
+
{:else}
|
|
303
|
+
<EventRsvp
|
|
304
|
+
{eventUri}
|
|
305
|
+
eventCid={eventData.cid ?? null}
|
|
306
|
+
initialRsvpStatus={data.viewerRsvpStatus}
|
|
307
|
+
initialRsvpRkey={data.viewerRsvpRkey}
|
|
308
|
+
spaceUri={data.spaceUri ?? null}
|
|
309
|
+
{adapter}
|
|
310
|
+
{viewer}
|
|
311
|
+
onrsvp={handleRsvp}
|
|
312
|
+
oncancel={handleRsvpCancel}
|
|
313
|
+
/>
|
|
314
|
+
{/if}
|
|
315
|
+
{/if}
|
|
316
|
+
|
|
317
|
+
<!-- Live stream -->
|
|
318
|
+
{#if isOngoing && streamPlaceHandle}
|
|
319
|
+
<div class="mt-8 mb-8">
|
|
320
|
+
<p
|
|
321
|
+
class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase"
|
|
322
|
+
>
|
|
323
|
+
Live now
|
|
324
|
+
</p>
|
|
325
|
+
<StreamPlacePlayer handle={streamPlaceHandle} title={eventData.name} />
|
|
326
|
+
</div>
|
|
327
|
+
{/if}
|
|
328
|
+
|
|
329
|
+
<!-- About + Comments -->
|
|
330
|
+
{#if descriptionHtml || hasComments}
|
|
331
|
+
<div class="mt-8 mb-8">
|
|
332
|
+
{#if descriptionHtml && hasComments}
|
|
333
|
+
<ToggleGroup
|
|
334
|
+
type="single"
|
|
335
|
+
bind:value={
|
|
336
|
+
() => aboutCommentsTab,
|
|
337
|
+
(val) => {
|
|
338
|
+
if (val === 'about' || val === 'comments') aboutCommentsTab = val;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
class="mb-4 w-fit"
|
|
342
|
+
size="xs"
|
|
343
|
+
>
|
|
344
|
+
<ToggleGroupItem value="about">About</ToggleGroupItem>
|
|
345
|
+
<ToggleGroupItem value="comments">Comments</ToggleGroupItem>
|
|
346
|
+
</ToggleGroup>
|
|
347
|
+
|
|
348
|
+
{#if aboutCommentsTab === 'about'}
|
|
349
|
+
<div
|
|
350
|
+
class="text-base-700 dark:text-base-300 prose dark:prose-invert prose-a:text-accent-600 dark:prose-a:text-accent-400 prose-a:hover:underline prose-a:no-underline max-w-none leading-relaxed wrap-break-word"
|
|
351
|
+
>
|
|
352
|
+
{@html descriptionHtml}
|
|
353
|
+
</div>
|
|
354
|
+
{:else if eventData.bskyPostRef?.uri}
|
|
355
|
+
<EventComments postUri={eventData.bskyPostRef.uri} />
|
|
356
|
+
{/if}
|
|
357
|
+
{:else if descriptionHtml}
|
|
358
|
+
<p
|
|
359
|
+
class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase"
|
|
360
|
+
>
|
|
361
|
+
About
|
|
362
|
+
</p>
|
|
363
|
+
<div
|
|
364
|
+
class="text-base-700 dark:text-base-300 prose dark:prose-invert prose-a:text-accent-600 dark:prose-a:text-accent-400 prose-a:hover:underline prose-a:no-underline max-w-none leading-relaxed wrap-break-word"
|
|
365
|
+
>
|
|
366
|
+
{@html descriptionHtml}
|
|
367
|
+
</div>
|
|
368
|
+
{:else if eventData.bskyPostRef?.uri}
|
|
369
|
+
<p
|
|
370
|
+
class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase"
|
|
371
|
+
>
|
|
372
|
+
Comments
|
|
373
|
+
</p>
|
|
374
|
+
<EventComments postUri={eventData.bskyPostRef.uri} />
|
|
375
|
+
{/if}
|
|
376
|
+
</div>
|
|
377
|
+
{/if}
|
|
378
|
+
|
|
379
|
+
<!-- Recording -->
|
|
380
|
+
{#if data.vod}
|
|
381
|
+
<div class="mt-8 mb-8">
|
|
382
|
+
<p
|
|
383
|
+
class="text-base-500 dark:text-base-400 mb-3 text-xs font-semibold tracking-wider uppercase"
|
|
384
|
+
>
|
|
385
|
+
Recording
|
|
386
|
+
</p>
|
|
387
|
+
<VodPlayer
|
|
388
|
+
playlistUrl={data.vod.playlistUrl}
|
|
389
|
+
title={eventData.name}
|
|
390
|
+
subtitlesUrl="/vods/{rkey}-karaoke.vtt"
|
|
391
|
+
bind:currentTime={vodCurrentTime}
|
|
392
|
+
bind:api={vodApi}
|
|
393
|
+
/>
|
|
394
|
+
</div>
|
|
395
|
+
{/if}
|
|
396
|
+
|
|
397
|
+
<EventLocationMap {locationData} {geoLocation} />
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<!-- Left column: sidebar info -->
|
|
401
|
+
<div class="order-3 space-y-6 md:order-0 md:col-start-1">
|
|
402
|
+
<EventHostedBy {hostProfile} {hostUrl} {did} {speakers} />
|
|
403
|
+
|
|
404
|
+
<EventLinksList uris={eventData.uris} />
|
|
405
|
+
|
|
406
|
+
<AddToCalendarButton {eventData} {eventUri} pageHref={pageUrl.href} />
|
|
407
|
+
|
|
408
|
+
<EventAttendees
|
|
409
|
+
bind:this={attendeesRef}
|
|
410
|
+
going={attendees.going}
|
|
411
|
+
interested={attendees.interested}
|
|
412
|
+
goingCount={attendees.goingCount}
|
|
413
|
+
interestedCount={attendees.interestedCount}
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<ShareModal
|
|
421
|
+
bind:open={showShareModal}
|
|
422
|
+
url={shareUrl}
|
|
423
|
+
title={shareModalTitle}
|
|
424
|
+
shareText={shareModalText}
|
|
425
|
+
eventName={eventData.name}
|
|
426
|
+
{ogImageUrl}
|
|
427
|
+
{canSetEventComments}
|
|
428
|
+
eventDid={did}
|
|
429
|
+
eventRkey={rkey}
|
|
430
|
+
eventDescription={eventData.description}
|
|
431
|
+
{adapter}
|
|
432
|
+
{viewer}
|
|
433
|
+
/>
|