@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,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Input, PopoverContent, PopoverRoot, PopoverTrigger } from '@foxui/core';
|
|
3
|
+
import { validateLink } from '../cal/helper.js';
|
|
4
|
+
|
|
5
|
+
type Link = { uri: string; name: string };
|
|
6
|
+
|
|
7
|
+
let { links = $bindable() }: { links: Link[] } = $props();
|
|
8
|
+
|
|
9
|
+
let showPopup = $state(false);
|
|
10
|
+
let newUri = $state('');
|
|
11
|
+
let newName = $state('');
|
|
12
|
+
let error = $state('');
|
|
13
|
+
|
|
14
|
+
function addLink() {
|
|
15
|
+
const raw = newUri.trim();
|
|
16
|
+
if (!raw) return;
|
|
17
|
+
const uri = validateLink(raw);
|
|
18
|
+
if (!uri) {
|
|
19
|
+
error = 'Please enter a valid URL';
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
links.push({ uri, name: newName.trim() });
|
|
23
|
+
newUri = '';
|
|
24
|
+
newName = '';
|
|
25
|
+
error = '';
|
|
26
|
+
showPopup = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function removeLink(index: number) {
|
|
30
|
+
links.splice(index, 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cancel() {
|
|
34
|
+
showPopup = false;
|
|
35
|
+
error = '';
|
|
36
|
+
newUri = '';
|
|
37
|
+
newName = '';
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
<p class="text-base-500 dark:text-base-400 mb-4 text-xs font-semibold tracking-wider uppercase">
|
|
43
|
+
Links
|
|
44
|
+
</p>
|
|
45
|
+
<div class="space-y-3">
|
|
46
|
+
{#each links as link, i (i)}
|
|
47
|
+
<div class="group flex items-center gap-1.5">
|
|
48
|
+
<svg
|
|
49
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
50
|
+
fill="none"
|
|
51
|
+
viewBox="0 0 24 24"
|
|
52
|
+
stroke-width="1.5"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
class="text-base-700 dark:text-base-300 size-3.5 shrink-0"
|
|
55
|
+
>
|
|
56
|
+
<path
|
|
57
|
+
stroke-linecap="round"
|
|
58
|
+
stroke-linejoin="round"
|
|
59
|
+
d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
|
|
60
|
+
/>
|
|
61
|
+
</svg>
|
|
62
|
+
<span class="text-base-700 dark:text-base-300 truncate text-sm">
|
|
63
|
+
{link.name || link.uri.replace(/^https?:\/\//, '')}
|
|
64
|
+
</span>
|
|
65
|
+
<Button
|
|
66
|
+
variant="ghost"
|
|
67
|
+
size="iconSm"
|
|
68
|
+
onclick={() => removeLink(i)}
|
|
69
|
+
class="ml-auto shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
70
|
+
>
|
|
71
|
+
<svg
|
|
72
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
73
|
+
viewBox="0 0 20 20"
|
|
74
|
+
fill="currentColor"
|
|
75
|
+
class="size-3.5"
|
|
76
|
+
>
|
|
77
|
+
<path
|
|
78
|
+
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"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
{/each}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="mt-3">
|
|
87
|
+
<PopoverRoot bind:open={showPopup}>
|
|
88
|
+
<PopoverTrigger>
|
|
89
|
+
<Button size="sm">
|
|
90
|
+
<svg
|
|
91
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
92
|
+
fill="none"
|
|
93
|
+
viewBox="0 0 24 24"
|
|
94
|
+
stroke-width="1.5"
|
|
95
|
+
stroke="currentColor"
|
|
96
|
+
class="size-4"
|
|
97
|
+
>
|
|
98
|
+
<path
|
|
99
|
+
stroke-linecap="round"
|
|
100
|
+
stroke-linejoin="round"
|
|
101
|
+
d="M12 4.5v15m7.5-7.5h-15"
|
|
102
|
+
/>
|
|
103
|
+
</svg>
|
|
104
|
+
Add link
|
|
105
|
+
</Button>
|
|
106
|
+
</PopoverTrigger>
|
|
107
|
+
<PopoverContent side="bottom" sideOffset={8} class="w-64 p-3">
|
|
108
|
+
<Input
|
|
109
|
+
type="url"
|
|
110
|
+
bind:value={newUri}
|
|
111
|
+
placeholder="https://..."
|
|
112
|
+
variant="secondary"
|
|
113
|
+
class="mb-2"
|
|
114
|
+
onkeydown={(e) => {
|
|
115
|
+
if (e.key === 'Enter') {
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
addLink();
|
|
118
|
+
}
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
<Input
|
|
122
|
+
type="text"
|
|
123
|
+
bind:value={newName}
|
|
124
|
+
placeholder="Label (optional)"
|
|
125
|
+
variant="secondary"
|
|
126
|
+
class="mb-2"
|
|
127
|
+
onkeydown={(e) => {
|
|
128
|
+
if (e.key === 'Enter') {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
addLink();
|
|
131
|
+
}
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
{#if error}
|
|
135
|
+
<p class="mb-2 text-xs text-red-500">{error}</p>
|
|
136
|
+
{/if}
|
|
137
|
+
<div class="flex justify-end gap-2">
|
|
138
|
+
<Button variant="ghost" size="sm" onclick={cancel}>Cancel</Button>
|
|
139
|
+
<Button onclick={addLink} size="sm" disabled={!newUri.trim()}>Add</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</PopoverContent>
|
|
142
|
+
</PopoverRoot>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Link = {
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
};
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
links: Link[];
|
|
7
|
+
};
|
|
8
|
+
declare const LinksSection: import("svelte").Component<$$ComponentProps, {}, "links">;
|
|
9
|
+
type LinksSection = ReturnType<typeof LinksSection>;
|
|
10
|
+
export default LinksSection;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Input, Modal } from '@foxui/core';
|
|
3
|
+
import { getLocationDisplayString, type EventLocation } from './types';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
location = $bindable(),
|
|
7
|
+
locationChanged = $bindable()
|
|
8
|
+
}: {
|
|
9
|
+
location: EventLocation | null;
|
|
10
|
+
locationChanged: boolean;
|
|
11
|
+
} = $props();
|
|
12
|
+
|
|
13
|
+
let showModal = $state(false);
|
|
14
|
+
let searchText = $state('');
|
|
15
|
+
let searching = $state(false);
|
|
16
|
+
let error = $state('');
|
|
17
|
+
let result: { displayName: string; location: EventLocation } | null = $state(null);
|
|
18
|
+
|
|
19
|
+
async function search() {
|
|
20
|
+
const q = searchText.trim();
|
|
21
|
+
if (!q) return;
|
|
22
|
+
error = '';
|
|
23
|
+
searching = true;
|
|
24
|
+
result = null;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch('/api/geocoding?q=' + encodeURIComponent(q));
|
|
28
|
+
if (!response.ok) throw new Error('response not ok');
|
|
29
|
+
const data: Record<string, unknown> = await response.json();
|
|
30
|
+
if (!data || data.error) throw new Error('no results');
|
|
31
|
+
|
|
32
|
+
const addr = (data.address || {}) as Record<string, string>;
|
|
33
|
+
const road = addr.road || '';
|
|
34
|
+
const houseNumber = addr.house_number || '';
|
|
35
|
+
const street = road ? (houseNumber ? `${road} ${houseNumber}` : road) : '';
|
|
36
|
+
const locality =
|
|
37
|
+
addr.city || addr.town || addr.village || addr.municipality || addr.hamlet || '';
|
|
38
|
+
const region = addr.state || addr.county || '';
|
|
39
|
+
const country = addr.country || '';
|
|
40
|
+
|
|
41
|
+
result = {
|
|
42
|
+
displayName: (data.display_name as string) || q,
|
|
43
|
+
location: {
|
|
44
|
+
...(street && { street }),
|
|
45
|
+
...(locality && { locality }),
|
|
46
|
+
...(region && { region }),
|
|
47
|
+
...(country && { country })
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
error = "Couldn't find that location.";
|
|
52
|
+
} finally {
|
|
53
|
+
searching = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function confirm() {
|
|
58
|
+
if (result) {
|
|
59
|
+
location = result.location;
|
|
60
|
+
locationChanged = true;
|
|
61
|
+
}
|
|
62
|
+
showModal = false;
|
|
63
|
+
searchText = '';
|
|
64
|
+
result = null;
|
|
65
|
+
error = '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function remove() {
|
|
69
|
+
location = null;
|
|
70
|
+
locationChanged = true;
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
{#if location}
|
|
75
|
+
<div class="mb-6 flex items-center gap-4">
|
|
76
|
+
<div
|
|
77
|
+
class="border-base-200 dark:border-base-700 bg-base-100 dark:bg-base-950/30 flex size-12 shrink-0 items-center justify-center rounded-xl border"
|
|
78
|
+
>
|
|
79
|
+
<svg
|
|
80
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
81
|
+
fill="none"
|
|
82
|
+
viewBox="0 0 24 24"
|
|
83
|
+
stroke-width="1.5"
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
class="text-base-900 dark:text-base-200 size-5"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
stroke-linecap="round"
|
|
89
|
+
stroke-linejoin="round"
|
|
90
|
+
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
91
|
+
/>
|
|
92
|
+
<path
|
|
93
|
+
stroke-linecap="round"
|
|
94
|
+
stroke-linejoin="round"
|
|
95
|
+
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
|
|
96
|
+
/>
|
|
97
|
+
</svg>
|
|
98
|
+
</div>
|
|
99
|
+
<p class="text-base-900 dark:text-base-50 flex-1 font-semibold">
|
|
100
|
+
{getLocationDisplayString(location)}
|
|
101
|
+
</p>
|
|
102
|
+
<Button variant="ghost" size="iconSm" onclick={remove} class="shrink-0">
|
|
103
|
+
<svg
|
|
104
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
105
|
+
viewBox="0 0 20 20"
|
|
106
|
+
fill="currentColor"
|
|
107
|
+
class="size-3.5"
|
|
108
|
+
>
|
|
109
|
+
<path
|
|
110
|
+
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"
|
|
111
|
+
/>
|
|
112
|
+
</svg>
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
{:else}
|
|
116
|
+
<div class="mb-6">
|
|
117
|
+
<Button variant="secondary" onclick={() => (showModal = true)}>
|
|
118
|
+
<svg
|
|
119
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
120
|
+
fill="none"
|
|
121
|
+
viewBox="0 0 24 24"
|
|
122
|
+
stroke-width="1.5"
|
|
123
|
+
stroke="currentColor"
|
|
124
|
+
class="size-4"
|
|
125
|
+
>
|
|
126
|
+
<path
|
|
127
|
+
stroke-linecap="round"
|
|
128
|
+
stroke-linejoin="round"
|
|
129
|
+
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
130
|
+
/>
|
|
131
|
+
<path
|
|
132
|
+
stroke-linecap="round"
|
|
133
|
+
stroke-linejoin="round"
|
|
134
|
+
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
Add location
|
|
138
|
+
</Button>
|
|
139
|
+
</div>
|
|
140
|
+
{/if}
|
|
141
|
+
|
|
142
|
+
<Modal bind:open={showModal}>
|
|
143
|
+
<p class="text-base-900 dark:text-base-50 text-lg font-semibold">Add location</p>
|
|
144
|
+
<form
|
|
145
|
+
onsubmit={(e) => {
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
search();
|
|
148
|
+
}}
|
|
149
|
+
class="mt-2"
|
|
150
|
+
>
|
|
151
|
+
<div class="flex gap-2">
|
|
152
|
+
<Input type="text" class="flex-1" bind:value={searchText} />
|
|
153
|
+
<Button type="submit" disabled={searching || !searchText.trim()}>
|
|
154
|
+
{searching ? 'Searching...' : 'Search'}
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
|
|
159
|
+
{#if error}
|
|
160
|
+
<p class="mt-3 text-sm text-red-600 dark:text-red-400">{error}</p>
|
|
161
|
+
{/if}
|
|
162
|
+
|
|
163
|
+
{#if result}
|
|
164
|
+
<div
|
|
165
|
+
class="border-base-200 dark:border-base-700 bg-base-50 dark:bg-base-900 mt-4 overflow-hidden rounded-xl border p-4"
|
|
166
|
+
>
|
|
167
|
+
<div class="flex items-start gap-3">
|
|
168
|
+
<svg
|
|
169
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
170
|
+
fill="none"
|
|
171
|
+
viewBox="0 0 24 24"
|
|
172
|
+
stroke-width="1.5"
|
|
173
|
+
stroke="currentColor"
|
|
174
|
+
class="text-base-500 mt-0.5 size-5 shrink-0"
|
|
175
|
+
>
|
|
176
|
+
<path
|
|
177
|
+
stroke-linecap="round"
|
|
178
|
+
stroke-linejoin="round"
|
|
179
|
+
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
|
180
|
+
/>
|
|
181
|
+
<path
|
|
182
|
+
stroke-linecap="round"
|
|
183
|
+
stroke-linejoin="round"
|
|
184
|
+
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
|
|
185
|
+
/>
|
|
186
|
+
</svg>
|
|
187
|
+
<div class="min-w-0 flex-1">
|
|
188
|
+
<p class="text-base-900 dark:text-base-50 font-medium">
|
|
189
|
+
{getLocationDisplayString(result.location)}
|
|
190
|
+
</p>
|
|
191
|
+
<p class="text-base-500 dark:text-base-400 mt-0.5 truncate text-xs">
|
|
192
|
+
{result.displayName}
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="mt-4 flex justify-end">
|
|
197
|
+
<Button onclick={confirm}>Use this location</Button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
<p class="text-base-400 dark:text-base-500 mt-4 text-xs">
|
|
203
|
+
Geocoding by <a
|
|
204
|
+
href="https://nominatim.openstreetmap.org/"
|
|
205
|
+
class="hover:text-base-600 dark:hover:text-base-400 underline"
|
|
206
|
+
target="_blank">Nominatim</a
|
|
207
|
+
>
|
|
208
|
+
/ ©
|
|
209
|
+
<a
|
|
210
|
+
href="https://www.openstreetmap.org/copyright"
|
|
211
|
+
class="hover:text-base-600 dark:hover:text-base-400 underline"
|
|
212
|
+
target="_blank">OpenStreetMap contributors</a
|
|
213
|
+
>
|
|
214
|
+
</p>
|
|
215
|
+
</Modal>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type EventLocation } from './types';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
location: EventLocation | null;
|
|
4
|
+
locationChanged: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const LocationSection: import("svelte").Component<$$ComponentProps, {}, "location" | "locationChanged">;
|
|
7
|
+
type LocationSection = ReturnType<typeof LocationSection>;
|
|
8
|
+
export default LocationSection;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Checkbox, Input, Modal, ToggleGroup, ToggleGroupItem } from '@foxui/core';
|
|
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 { buildThumbnailMedia, renderPresetThumbnail } from './save';
|
|
8
|
+
import { hashSeed } from '../thumbnails/designs.js';
|
|
9
|
+
import type { EditorAdapter, EditorViewer } from './adapter';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
open = $bindable(),
|
|
13
|
+
rkey,
|
|
14
|
+
eventData,
|
|
15
|
+
isNew,
|
|
16
|
+
name,
|
|
17
|
+
startsAt,
|
|
18
|
+
endsAt,
|
|
19
|
+
mode,
|
|
20
|
+
timezone,
|
|
21
|
+
description,
|
|
22
|
+
links,
|
|
23
|
+
location,
|
|
24
|
+
thumbnailDateStr,
|
|
25
|
+
thumbnailFile,
|
|
26
|
+
thumbnailChanged,
|
|
27
|
+
selectedPreset,
|
|
28
|
+
accent,
|
|
29
|
+
adapter,
|
|
30
|
+
viewer
|
|
31
|
+
}: {
|
|
32
|
+
open: boolean;
|
|
33
|
+
rkey: string;
|
|
34
|
+
eventData: FlatEventRecord | null;
|
|
35
|
+
isNew: boolean;
|
|
36
|
+
name: string;
|
|
37
|
+
startsAt: string;
|
|
38
|
+
endsAt: string;
|
|
39
|
+
mode: EventMode;
|
|
40
|
+
timezone: string;
|
|
41
|
+
description: string;
|
|
42
|
+
links: Array<{ uri: string; name: string }>;
|
|
43
|
+
location: EventLocation | null;
|
|
44
|
+
thumbnailDateStr: string;
|
|
45
|
+
thumbnailFile: File | null;
|
|
46
|
+
thumbnailChanged: boolean;
|
|
47
|
+
selectedPreset: string | null;
|
|
48
|
+
accent: string;
|
|
49
|
+
adapter: EditorAdapter;
|
|
50
|
+
viewer: EditorViewer;
|
|
51
|
+
} = $props();
|
|
52
|
+
|
|
53
|
+
let interval = $state(1);
|
|
54
|
+
let unit: 'days' | 'weeks' | 'months' | 'years' = $state('weeks');
|
|
55
|
+
let count = $state(4);
|
|
56
|
+
let numberInTitle = $state(false);
|
|
57
|
+
let creating = $state(false);
|
|
58
|
+
let errorMsg: string | null = $state(null);
|
|
59
|
+
let created = $state(0);
|
|
60
|
+
|
|
61
|
+
let titleNumberMatch = $derived(name.match(/#?(\d+)\s*$/));
|
|
62
|
+
let detectedStartNumber = $derived(titleNumberMatch ? parseInt(titleNumberMatch[1]) : null);
|
|
63
|
+
|
|
64
|
+
$effect(() => {
|
|
65
|
+
if (detectedStartNumber !== null) numberInTitle = true;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function handleCreate() {
|
|
69
|
+
if (!name.trim() || !startsAt || !viewer.isLoggedIn || !viewer.did) return;
|
|
70
|
+
|
|
71
|
+
creating = true;
|
|
72
|
+
errorMsg = null;
|
|
73
|
+
created = 0;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Recurring instances advance by wall-clock duration (e.g. "every week
|
|
77
|
+
// at 10am"), so operate on CalendarDateTime — not absolute instants —
|
|
78
|
+
// to preserve the wall time across DST transitions.
|
|
79
|
+
const baseStart = parseDateTime(startsAt);
|
|
80
|
+
const baseEnd = endsAt ? parseDateTime(endsAt) : null;
|
|
81
|
+
const durationMs = baseEnd
|
|
82
|
+
? baseEnd.toDate(timezone).getTime() - baseStart.toDate(timezone).getTime()
|
|
83
|
+
: 0;
|
|
84
|
+
const baseName =
|
|
85
|
+
numberInTitle && titleNumberMatch
|
|
86
|
+
? name.replace(/#?\d+\s*$/, '').trimEnd()
|
|
87
|
+
: name.trim();
|
|
88
|
+
const startNum = detectedStartNumber ?? 1;
|
|
89
|
+
const hasHash = titleNumberMatch ? titleNumberMatch[0].includes('#') : false;
|
|
90
|
+
|
|
91
|
+
// Generate thumbnail from preset if selected and no custom upload.
|
|
92
|
+
let fileForUpload = thumbnailFile;
|
|
93
|
+
let hasNewThumbnail = thumbnailChanged;
|
|
94
|
+
if (selectedPreset && !fileForUpload) {
|
|
95
|
+
const rendered = await renderPresetThumbnail({
|
|
96
|
+
design: selectedPreset,
|
|
97
|
+
seed: hashSeed(rkey),
|
|
98
|
+
name,
|
|
99
|
+
dateStr: thumbnailDateStr,
|
|
100
|
+
accent
|
|
101
|
+
});
|
|
102
|
+
if (rendered) {
|
|
103
|
+
fileForUpload = rendered;
|
|
104
|
+
hasNewThumbnail = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const existingMedia = (eventData?.media ?? []) as Array<Record<string, unknown>>;
|
|
109
|
+
const media = await buildThumbnailMedia({
|
|
110
|
+
isNew,
|
|
111
|
+
thumbnailChanged: hasNewThumbnail,
|
|
112
|
+
thumbnailFile: fileForUpload,
|
|
113
|
+
existingMedia,
|
|
114
|
+
uploadBlob: (blob) =>
|
|
115
|
+
adapter.uploadBlob(blob) as unknown as Promise<Record<string, unknown>>
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const parentUri = `at://${viewer.did}/community.lexicon.calendar.event/${rkey}`;
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < count; i++) {
|
|
121
|
+
const offset = i + 1;
|
|
122
|
+
const step = offset * interval;
|
|
123
|
+
const eventStart =
|
|
124
|
+
unit === 'days'
|
|
125
|
+
? baseStart.add({ days: step })
|
|
126
|
+
: unit === 'weeks'
|
|
127
|
+
? baseStart.add({ weeks: step })
|
|
128
|
+
: unit === 'months'
|
|
129
|
+
? baseStart.add({ months: step })
|
|
130
|
+
: baseStart.add({ years: step });
|
|
131
|
+
|
|
132
|
+
const eventStartIso = eventStart.toDate(timezone).toISOString();
|
|
133
|
+
// Preserve the original absolute duration (handles events that
|
|
134
|
+
// span midnight or odd wall-clock lengths correctly).
|
|
135
|
+
const eventEndIso = durationMs
|
|
136
|
+
? new Date(eventStart.toDate(timezone).getTime() + durationMs).toISOString()
|
|
137
|
+
: null;
|
|
138
|
+
|
|
139
|
+
let eventName = baseName;
|
|
140
|
+
if (numberInTitle) {
|
|
141
|
+
const num = startNum + (i + 1);
|
|
142
|
+
eventName = hasHash ? `${baseName} #${num}` : `${baseName} ${num}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const newRkey = TID.now();
|
|
146
|
+
const record: Record<string, unknown> = {
|
|
147
|
+
$type: 'community.lexicon.calendar.event',
|
|
148
|
+
createdWith: 'https://atmo.rsvp',
|
|
149
|
+
name: eventName,
|
|
150
|
+
mode: `community.lexicon.calendar.event#${mode}`,
|
|
151
|
+
status: 'community.lexicon.calendar.event#scheduled',
|
|
152
|
+
startsAt: eventStartIso,
|
|
153
|
+
timezone,
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
recurringEventOf: parentUri
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const trimmedDescription = description.trim();
|
|
159
|
+
if (trimmedDescription) record.description = trimmedDescription;
|
|
160
|
+
if (eventEndIso) record.endsAt = eventEndIso;
|
|
161
|
+
if (media) record.media = media;
|
|
162
|
+
if (links.length > 0) record.uris = links;
|
|
163
|
+
if (location) {
|
|
164
|
+
record.locations = [
|
|
165
|
+
{
|
|
166
|
+
$type: 'community.lexicon.location.address',
|
|
167
|
+
...location
|
|
168
|
+
}
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await adapter.putRecord({
|
|
174
|
+
collection: 'community.lexicon.calendar.event',
|
|
175
|
+
rkey: newRkey,
|
|
176
|
+
record
|
|
177
|
+
});
|
|
178
|
+
await adapter.notifyUpdate?.(result.uri);
|
|
179
|
+
created = i + 1;
|
|
180
|
+
} catch {
|
|
181
|
+
errorMsg = `Failed to create event ${i + 1}. Stopping.`;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
open = false;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error('Failed to create recurring events:', e);
|
|
189
|
+
errorMsg = 'Failed to create recurring events. Please try again.';
|
|
190
|
+
} finally {
|
|
191
|
+
creating = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<Modal bind:open>
|
|
197
|
+
<p class="text-base-900 dark:text-base-50 text-lg font-semibold">Add recurring events</p>
|
|
198
|
+
<p class="text-base-500 dark:text-base-400 mt-1 text-sm">
|
|
199
|
+
Create multiple copies of this event at regular intervals.
|
|
200
|
+
</p>
|
|
201
|
+
|
|
202
|
+
<div class="mt-4 space-y-4">
|
|
203
|
+
<div>
|
|
204
|
+
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
205
|
+
<label class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium">
|
|
206
|
+
Number of events to create
|
|
207
|
+
</label>
|
|
208
|
+
<Input type="number" bind:value={count} min={1} max={52} class="w-24" />
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div>
|
|
212
|
+
<!-- svelte-ignore a11y_label_has_associated_control -->
|
|
213
|
+
<label class="text-base-700 dark:text-base-300 mb-1 block text-sm font-medium">
|
|
214
|
+
Repeat every
|
|
215
|
+
</label>
|
|
216
|
+
<div class="flex items-center gap-2">
|
|
217
|
+
<Input type="number" bind:value={interval} min={1} max={99} class="w-20" />
|
|
218
|
+
<ToggleGroup type="single" bind:value={unit}>
|
|
219
|
+
<ToggleGroupItem value="days">days</ToggleGroupItem>
|
|
220
|
+
<ToggleGroupItem value="weeks">weeks</ToggleGroupItem>
|
|
221
|
+
<ToggleGroupItem value="months">months</ToggleGroupItem>
|
|
222
|
+
<ToggleGroupItem value="years">years</ToggleGroupItem>
|
|
223
|
+
</ToggleGroup>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div>
|
|
228
|
+
<div class="flex items-center gap-2">
|
|
229
|
+
<Checkbox bind:checked={numberInTitle} sizeVariant="sm" />
|
|
230
|
+
<span class="text-base-700 dark:text-base-300 text-sm font-medium">Number in title</span>
|
|
231
|
+
</div>
|
|
232
|
+
<p class="text-base-500 dark:text-base-400 mt-1 text-xs">
|
|
233
|
+
{#if numberInTitle && detectedStartNumber !== null}
|
|
234
|
+
Titles will count up from #{detectedStartNumber + 1}
|
|
235
|
+
{:else if numberInTitle}
|
|
236
|
+
A number will be appended to each title
|
|
237
|
+
{:else}
|
|
238
|
+
Append a number to each event title
|
|
239
|
+
{/if}
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{#if errorMsg}
|
|
245
|
+
<p class="mt-4 text-sm text-red-600 dark:text-red-400">{errorMsg}</p>
|
|
246
|
+
{/if}
|
|
247
|
+
|
|
248
|
+
{#if creating && created > 0}
|
|
249
|
+
<p class="text-base-500 dark:text-base-400 mt-4 text-sm">
|
|
250
|
+
Created {created} of {count} events...
|
|
251
|
+
</p>
|
|
252
|
+
{/if}
|
|
253
|
+
|
|
254
|
+
{#if created > 0 && !creating}
|
|
255
|
+
<p class="mt-4 text-sm text-green-600 dark:text-green-400">
|
|
256
|
+
Successfully created {created} recurring events!
|
|
257
|
+
</p>
|
|
258
|
+
{/if}
|
|
259
|
+
|
|
260
|
+
<div class="mt-4 flex justify-end gap-2">
|
|
261
|
+
<Button variant="secondary" onclick={() => (open = false)} disabled={creating}>
|
|
262
|
+
{created > 0 && !creating ? 'Close' : 'Cancel'}
|
|
263
|
+
</Button>
|
|
264
|
+
{#if !created || creating}
|
|
265
|
+
<Button onclick={handleCreate} disabled={creating || count < 1}>
|
|
266
|
+
{creating ? `Creating...` : `Create ${count} event${count === 1 ? '' : 's'}`}
|
|
267
|
+
</Button>
|
|
268
|
+
{/if}
|
|
269
|
+
</div>
|
|
270
|
+
</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;
|