@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,231 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import { DatePicker } from 'bits-ui';
|
|
4
|
+
import { CalendarDate, type DateValue } from '@internationalized/date';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
value = $bindable(''),
|
|
9
|
+
required = false,
|
|
10
|
+
minValue = '',
|
|
11
|
+
locale = 'en',
|
|
12
|
+
onSelect
|
|
13
|
+
}: {
|
|
14
|
+
value: string;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
minValue?: string;
|
|
17
|
+
locale?: string;
|
|
18
|
+
onSelect?: () => void;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
let isOpen = $state(false);
|
|
22
|
+
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const currentYear = now.getFullYear();
|
|
25
|
+
const yearRange = Array.from({ length: 7 }, (_, i) => currentYear - 1 + i);
|
|
26
|
+
const today = new Date();
|
|
27
|
+
const todayDay = today.getDate();
|
|
28
|
+
const todayMonth = today.getMonth() + 1;
|
|
29
|
+
const todayYear = today.getFullYear();
|
|
30
|
+
|
|
31
|
+
let internalValue: CalendarDate | undefined = $state(undefined);
|
|
32
|
+
|
|
33
|
+
function parseDateStr(str: string): CalendarDate | undefined {
|
|
34
|
+
if (!str) return undefined;
|
|
35
|
+
const [yearStr, monthStr, dayStr] = str.split('-');
|
|
36
|
+
const year = parseInt(yearStr, 10);
|
|
37
|
+
const month = parseInt(monthStr, 10);
|
|
38
|
+
const day = parseInt(dayStr, 10);
|
|
39
|
+
if (isNaN(year) || isNaN(month) || isNaN(day)) return undefined;
|
|
40
|
+
return new CalendarDate(year, month, day);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatDateStr(dt: CalendarDate): string {
|
|
44
|
+
const y = String(dt.year).padStart(4, '0');
|
|
45
|
+
const m = String(dt.month).padStart(2, '0');
|
|
46
|
+
const d = String(dt.day).padStart(2, '0');
|
|
47
|
+
return `${y}-${m}-${d}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let internalMinValue: CalendarDate | undefined = $derived.by(() => {
|
|
51
|
+
return parseDateStr(minValue);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
const parsed = parseDateStr(value);
|
|
56
|
+
untrack(() => {
|
|
57
|
+
if (parsed) {
|
|
58
|
+
if (
|
|
59
|
+
!internalValue ||
|
|
60
|
+
parsed.year !== internalValue.year ||
|
|
61
|
+
parsed.month !== internalValue.month ||
|
|
62
|
+
parsed.day !== internalValue.day
|
|
63
|
+
) {
|
|
64
|
+
internalValue = parsed;
|
|
65
|
+
previousValue = parsed;
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
internalValue = undefined;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let previousValue: CalendarDate | undefined = $state(undefined);
|
|
74
|
+
|
|
75
|
+
function handleValueChange(newVal: DateValue | undefined) {
|
|
76
|
+
if (newVal && newVal instanceof CalendarDate) {
|
|
77
|
+
previousValue = newVal;
|
|
78
|
+
internalValue = newVal;
|
|
79
|
+
value = formatDateStr(newVal);
|
|
80
|
+
} else if (!newVal && previousValue) {
|
|
81
|
+
// Prevent deselection — restore previous value
|
|
82
|
+
internalValue = previousValue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleOpenChangeComplete(open: boolean) {
|
|
87
|
+
if (!open && internalValue) {
|
|
88
|
+
onSelect?.();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let displayText = $derived.by(() => {
|
|
93
|
+
if (!internalValue) return '';
|
|
94
|
+
const date = new Date(internalValue.year, internalValue.month - 1, internalValue.day);
|
|
95
|
+
const opts: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', day: 'numeric' };
|
|
96
|
+
if (internalValue.year !== currentYear) {
|
|
97
|
+
opts.year = 'numeric';
|
|
98
|
+
}
|
|
99
|
+
return date.toLocaleDateString(locale, opts);
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<DatePicker.Root
|
|
104
|
+
bind:value={internalValue}
|
|
105
|
+
bind:open={isOpen}
|
|
106
|
+
onValueChange={handleValueChange}
|
|
107
|
+
onOpenChangeComplete={handleOpenChangeComplete}
|
|
108
|
+
minValue={internalMinValue}
|
|
109
|
+
granularity="day"
|
|
110
|
+
fixedWeeks={true}
|
|
111
|
+
weekdayFormat="short"
|
|
112
|
+
{locale}
|
|
113
|
+
{required}
|
|
114
|
+
>
|
|
115
|
+
<DatePicker.Trigger
|
|
116
|
+
class="border-base-300 bg-base-100 text-base-900 focus-within:border-accent-500 dark:border-base-700 dark:bg-base-800 dark:text-base-100 dark:focus-within:border-accent-400 flex w-full min-w-[8.5rem] cursor-pointer items-center rounded-xl border px-2.5 py-1.5 text-sm transition-colors"
|
|
117
|
+
>
|
|
118
|
+
<span class="select-none">
|
|
119
|
+
{#if displayText}
|
|
120
|
+
{displayText}
|
|
121
|
+
{:else}
|
|
122
|
+
<span class="text-base-400 dark:text-base-500">Select date</span>
|
|
123
|
+
{/if}
|
|
124
|
+
</span>
|
|
125
|
+
</DatePicker.Trigger>
|
|
126
|
+
|
|
127
|
+
<DatePicker.Content
|
|
128
|
+
sideOffset={8}
|
|
129
|
+
class="border-base-200 bg-base-50 dark:border-base-700 dark:bg-base-900 z-50 rounded-2xl border p-4 shadow-lg"
|
|
130
|
+
>
|
|
131
|
+
<DatePicker.Calendar>
|
|
132
|
+
{#snippet children({ months, weekdays })}
|
|
133
|
+
<DatePicker.Header class="flex items-center justify-between">
|
|
134
|
+
<DatePicker.PrevButton
|
|
135
|
+
class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg"
|
|
136
|
+
>
|
|
137
|
+
<svg
|
|
138
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
139
|
+
viewBox="0 0 20 20"
|
|
140
|
+
fill="currentColor"
|
|
141
|
+
class="size-5"
|
|
142
|
+
>
|
|
143
|
+
<path
|
|
144
|
+
fill-rule="evenodd"
|
|
145
|
+
d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
|
|
146
|
+
clip-rule="evenodd"
|
|
147
|
+
/>
|
|
148
|
+
</svg>
|
|
149
|
+
</DatePicker.PrevButton>
|
|
150
|
+
|
|
151
|
+
<div class="flex items-center gap-1.5">
|
|
152
|
+
<DatePicker.MonthSelect
|
|
153
|
+
monthFormat="long"
|
|
154
|
+
class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none"
|
|
155
|
+
/>
|
|
156
|
+
<DatePicker.YearSelect
|
|
157
|
+
years={yearRange}
|
|
158
|
+
class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<DatePicker.NextButton
|
|
163
|
+
class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg"
|
|
164
|
+
>
|
|
165
|
+
<svg
|
|
166
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
167
|
+
viewBox="0 0 20 20"
|
|
168
|
+
fill="currentColor"
|
|
169
|
+
class="size-5"
|
|
170
|
+
>
|
|
171
|
+
<path
|
|
172
|
+
fill-rule="evenodd"
|
|
173
|
+
d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
|
|
174
|
+
clip-rule="evenodd"
|
|
175
|
+
/>
|
|
176
|
+
</svg>
|
|
177
|
+
</DatePicker.NextButton>
|
|
178
|
+
</DatePicker.Header>
|
|
179
|
+
|
|
180
|
+
{#each months as month (month.value.month)}
|
|
181
|
+
<DatePicker.Grid class="mt-3 w-full">
|
|
182
|
+
<DatePicker.GridHead>
|
|
183
|
+
<DatePicker.GridRow class="flex w-full">
|
|
184
|
+
{#each weekdays as weekday, i (i)}
|
|
185
|
+
<DatePicker.HeadCell
|
|
186
|
+
class="text-base-400 dark:text-base-500 flex-1 text-center text-xs font-medium"
|
|
187
|
+
>
|
|
188
|
+
{weekday}
|
|
189
|
+
</DatePicker.HeadCell>
|
|
190
|
+
{/each}
|
|
191
|
+
</DatePicker.GridRow>
|
|
192
|
+
</DatePicker.GridHead>
|
|
193
|
+
|
|
194
|
+
<DatePicker.GridBody>
|
|
195
|
+
{#each month.weeks as week, weekIndex (weekIndex)}
|
|
196
|
+
<DatePicker.GridRow class="flex w-full">
|
|
197
|
+
{#each week as day (day.toString())}
|
|
198
|
+
<DatePicker.Cell date={day} month={month.value} class="flex-1 p-0.5">
|
|
199
|
+
<DatePicker.Day>
|
|
200
|
+
{#snippet children({ selected, disabled, day: dayText })}
|
|
201
|
+
<div
|
|
202
|
+
class="relative flex size-9 items-center justify-center rounded-lg text-sm
|
|
203
|
+
{selected
|
|
204
|
+
? 'bg-accent-500 font-medium text-white'
|
|
205
|
+
: disabled
|
|
206
|
+
? 'text-base-300 dark:text-base-600 pointer-events-none'
|
|
207
|
+
: day.month !== month.value.month
|
|
208
|
+
? 'text-base-300 dark:text-base-600'
|
|
209
|
+
: 'text-base-700 hover:bg-base-200 dark:text-base-300 dark:hover:bg-base-700'}"
|
|
210
|
+
>
|
|
211
|
+
{dayText}
|
|
212
|
+
{#if day.day === todayDay && day.month === todayMonth && day.year === todayYear}
|
|
213
|
+
<span
|
|
214
|
+
class="bg-accent-500 absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full"
|
|
215
|
+
class:bg-white={selected}
|
|
216
|
+
></span>
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
219
|
+
{/snippet}
|
|
220
|
+
</DatePicker.Day>
|
|
221
|
+
</DatePicker.Cell>
|
|
222
|
+
{/each}
|
|
223
|
+
</DatePicker.GridRow>
|
|
224
|
+
{/each}
|
|
225
|
+
</DatePicker.GridBody>
|
|
226
|
+
</DatePicker.Grid>
|
|
227
|
+
{/each}
|
|
228
|
+
{/snippet}
|
|
229
|
+
</DatePicker.Calendar>
|
|
230
|
+
</DatePicker.Content>
|
|
231
|
+
</DatePicker.Root>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DatePicker } from 'bits-ui';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
value: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
minValue?: string;
|
|
6
|
+
locale?: string;
|
|
7
|
+
onSelect?: () => void;
|
|
8
|
+
};
|
|
9
|
+
declare const DatePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
10
|
+
type DatePicker = ReturnType<typeof DatePicker>;
|
|
11
|
+
export default DatePicker;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
import DatePickerField from './DatePicker.svelte';
|
|
4
|
+
import TimePicker from './TimePicker.svelte';
|
|
5
|
+
import { untrack } from 'svelte';
|
|
6
|
+
import { BROWSER as browser } from 'esm-env';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
value = $bindable(''),
|
|
10
|
+
required = false,
|
|
11
|
+
minValue = '',
|
|
12
|
+
referenceTime = ''
|
|
13
|
+
}: {
|
|
14
|
+
value: string;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
minValue?: string;
|
|
17
|
+
referenceTime?: string;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
let datePart = $state('');
|
|
21
|
+
let timePart = $state('00:00');
|
|
22
|
+
let timeEl: HTMLDivElement | undefined = $state(undefined);
|
|
23
|
+
|
|
24
|
+
const locale = browser ? navigator.language || 'en' : 'en';
|
|
25
|
+
let minDatePart = $derived(minValue ? minValue.split('T')[0] || '' : '');
|
|
26
|
+
let refTimePart = $derived.by(() => {
|
|
27
|
+
if (!referenceTime) return '';
|
|
28
|
+
const [refDate, refTime] = referenceTime.split('T');
|
|
29
|
+
if (refDate && refDate === datePart && refTime) return refTime;
|
|
30
|
+
return '';
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Default to current date/time rounded up to the next hour when no initial value
|
|
34
|
+
if (browser && !value) {
|
|
35
|
+
const now = new Date();
|
|
36
|
+
const rounded = new Date(now);
|
|
37
|
+
rounded.setMinutes(0, 0, 0);
|
|
38
|
+
rounded.setHours(rounded.getHours() + 1);
|
|
39
|
+
|
|
40
|
+
const yyyy = rounded.getFullYear();
|
|
41
|
+
const mm = String(rounded.getMonth() + 1).padStart(2, '0');
|
|
42
|
+
const dd = String(rounded.getDate()).padStart(2, '0');
|
|
43
|
+
const hh = String(rounded.getHours()).padStart(2, '0');
|
|
44
|
+
const min = String(rounded.getMinutes()).padStart(2, '0');
|
|
45
|
+
|
|
46
|
+
const defaultDate = `${yyyy}-${mm}-${dd}`;
|
|
47
|
+
const defaultTime = `${hh}:${min}`;
|
|
48
|
+
datePart = defaultDate;
|
|
49
|
+
timePart = defaultTime;
|
|
50
|
+
value = `${defaultDate}T${defaultTime}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Sync external value -> date/time parts
|
|
54
|
+
$effect(() => {
|
|
55
|
+
const v = value;
|
|
56
|
+
untrack(() => {
|
|
57
|
+
if (v) {
|
|
58
|
+
const [d, t] = v.split('T');
|
|
59
|
+
if (d && d !== datePart) datePart = d;
|
|
60
|
+
if (t && t !== timePart) timePart = t;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Sync date/time parts -> external value
|
|
66
|
+
$effect(() => {
|
|
67
|
+
const d = datePart;
|
|
68
|
+
const t = timePart;
|
|
69
|
+
untrack(() => {
|
|
70
|
+
if (d) {
|
|
71
|
+
const newVal = `${d}T${t || '00:00'}`;
|
|
72
|
+
if (newVal !== value) value = newVal;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function focusTime() {
|
|
78
|
+
// Small delay to let the popover finish closing
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (timeEl) {
|
|
81
|
+
const segment = timeEl.querySelector('[data-segment]');
|
|
82
|
+
if (segment instanceof HTMLElement) {
|
|
83
|
+
segment.focus();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, 50);
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<div class="flex items-center gap-1.5">
|
|
91
|
+
<DatePickerField
|
|
92
|
+
bind:value={datePart}
|
|
93
|
+
{required}
|
|
94
|
+
minValue={minDatePart}
|
|
95
|
+
{locale}
|
|
96
|
+
onSelect={focusTime}
|
|
97
|
+
/>
|
|
98
|
+
<div bind:this={timeEl}>
|
|
99
|
+
<TimePicker bind:value={timePart} {locale} referenceTime={refTimePart} />
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value: string;
|
|
3
|
+
required?: boolean;
|
|
4
|
+
minValue?: string;
|
|
5
|
+
referenceTime?: string;
|
|
6
|
+
};
|
|
7
|
+
declare const DateTimePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
|
|
8
|
+
type DateTimePicker = ReturnType<typeof DateTimePicker>;
|
|
9
|
+
export default DateTimePicker;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AttendeeInfo } from './contrail.js';
|
|
3
|
+
import { Avatar as FoxAvatar } from '@foxui/core';
|
|
4
|
+
import { scale } from 'svelte/transition';
|
|
5
|
+
import { flip } from 'svelte/animate';
|
|
6
|
+
import { Modal } from '@foxui/core';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
going = [],
|
|
10
|
+
interested = [],
|
|
11
|
+
goingCount: initialGoingCount = going.length,
|
|
12
|
+
interestedCount: initialInterestedCount = interested.length
|
|
13
|
+
}: {
|
|
14
|
+
going?: AttendeeInfo[];
|
|
15
|
+
interested?: AttendeeInfo[];
|
|
16
|
+
goingCount?: number;
|
|
17
|
+
interestedCount?: number;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
let goingCountOverride: number | null = $state(null);
|
|
21
|
+
let interestedCountOverride: number | null = $state(null);
|
|
22
|
+
let goingAttendeesOverride: AttendeeInfo[] | null = $state(null);
|
|
23
|
+
let interestedAttendeesOverride: AttendeeInfo[] | null = $state(null);
|
|
24
|
+
|
|
25
|
+
let modalOpen = $state(false);
|
|
26
|
+
let modalGroup: 'going' | 'interested' = $state('going');
|
|
27
|
+
|
|
28
|
+
const MAX_AVATARS = 18;
|
|
29
|
+
|
|
30
|
+
let goingCount = $derived(goingCountOverride ?? initialGoingCount);
|
|
31
|
+
let interestedCount = $derived(interestedCountOverride ?? initialInterestedCount);
|
|
32
|
+
let goingAttendees = $derived(goingAttendeesOverride ?? going);
|
|
33
|
+
let interestedAttendees = $derived(interestedAttendeesOverride ?? interested);
|
|
34
|
+
|
|
35
|
+
let totalCount = $derived(goingCount + interestedCount);
|
|
36
|
+
|
|
37
|
+
let goingDisplay = $derived(goingAttendees.slice(0, MAX_AVATARS));
|
|
38
|
+
let goingOverflow = $derived(goingCount - goingDisplay.length);
|
|
39
|
+
|
|
40
|
+
let interestedDisplay = $derived(interestedAttendees.slice(0, MAX_AVATARS));
|
|
41
|
+
let interestedOverflow = $derived(interestedCount - interestedDisplay.length);
|
|
42
|
+
|
|
43
|
+
let modalAttendees = $derived(modalGroup === 'going' ? goingAttendees : interestedAttendees);
|
|
44
|
+
let modalTitle = $derived(modalGroup === 'going' ? 'Going' : 'Interested');
|
|
45
|
+
|
|
46
|
+
function openModal(group: 'going' | 'interested') {
|
|
47
|
+
modalGroup = group;
|
|
48
|
+
modalOpen = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function addAttendee(attendee: AttendeeInfo) {
|
|
52
|
+
const nextGoing = goingAttendees.filter((a) => a.did !== attendee.did);
|
|
53
|
+
const nextInterested = interestedAttendees.filter((a) => a.did !== attendee.did);
|
|
54
|
+
|
|
55
|
+
// Remove from both lists first (in case of status change)
|
|
56
|
+
if (attendee.status === 'going') {
|
|
57
|
+
goingAttendeesOverride = [attendee, ...nextGoing];
|
|
58
|
+
interestedAttendeesOverride = nextInterested;
|
|
59
|
+
goingCountOverride = goingAttendeesOverride.length;
|
|
60
|
+
interestedCountOverride = interestedAttendeesOverride.length;
|
|
61
|
+
} else if (attendee.status === 'interested') {
|
|
62
|
+
goingAttendeesOverride = nextGoing;
|
|
63
|
+
interestedAttendeesOverride = [attendee, ...nextInterested];
|
|
64
|
+
goingCountOverride = goingAttendeesOverride.length;
|
|
65
|
+
interestedCountOverride = interestedAttendeesOverride.length;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function thumbnail(url: string | undefined) {
|
|
70
|
+
return url?.replace('/avatar/', '/avatar_thumbnail/');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function removeAttendee(did: string) {
|
|
74
|
+
const wasGoing = goingAttendees.some((a) => a.did === did);
|
|
75
|
+
const wasInterested = interestedAttendees.some((a) => a.did === did);
|
|
76
|
+
goingAttendeesOverride = goingAttendees.filter((a) => a.did !== did);
|
|
77
|
+
interestedAttendeesOverride = interestedAttendees.filter((a) => a.did !== did);
|
|
78
|
+
if (wasGoing) goingCountOverride = goingAttendeesOverride.length;
|
|
79
|
+
if (wasInterested) interestedCountOverride = interestedAttendeesOverride.length;
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
{#if totalCount > 0}
|
|
84
|
+
<div class="mb-2">
|
|
85
|
+
{#if goingCount > 0}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
class="hover:bg-base-100 dark:hover:bg-base-800/50 -mx-2 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors"
|
|
89
|
+
onclick={() => openModal('going')}
|
|
90
|
+
>
|
|
91
|
+
<p class="text-base-900 dark:text-base-50 mb-2 text-sm">
|
|
92
|
+
<span class="font-bold">{goingCount}</span>
|
|
93
|
+
<span
|
|
94
|
+
class="text-base-500 dark:text-base-400 text-xs font-semibold tracking-wider uppercase"
|
|
95
|
+
>Going</span
|
|
96
|
+
>
|
|
97
|
+
</p>
|
|
98
|
+
<div class="flex items-center">
|
|
99
|
+
<div class="flex flex-wrap -space-y-2 -space-x-4 pr-4">
|
|
100
|
+
{#each goingDisplay as person (person.did)}
|
|
101
|
+
<div
|
|
102
|
+
animate:flip={{ duration: 300 }}
|
|
103
|
+
in:scale={{ duration: 300, start: 0.5 }}
|
|
104
|
+
out:scale={{ duration: 200, start: 0.5 }}
|
|
105
|
+
>
|
|
106
|
+
<FoxAvatar
|
|
107
|
+
src={thumbnail(person.avatar)}
|
|
108
|
+
alt={person.name}
|
|
109
|
+
class="border-base-100 dark:border-base-900 size-12 border-2"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
{/each}
|
|
113
|
+
{#if goingOverflow > 0}
|
|
114
|
+
<span
|
|
115
|
+
class="bg-base-200 dark:bg-base-800 text-base-950 dark:text-base-100 border-base-100 dark:border-base-900 z-10 inline-flex size-12 items-center justify-center rounded-full border-2 text-sm font-semibold"
|
|
116
|
+
>
|
|
117
|
+
+{goingOverflow}
|
|
118
|
+
</span>
|
|
119
|
+
{/if}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</button>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
{#if interestedCount > 0}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="hover:bg-base-100 dark:hover:bg-base-800/50 -mx-2 mt-4 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors"
|
|
129
|
+
onclick={() => openModal('interested')}
|
|
130
|
+
>
|
|
131
|
+
<p class="text-base-900 dark:text-base-50 mb-2 text-sm">
|
|
132
|
+
<span class="font-bold">{interestedCount}</span>
|
|
133
|
+
<span
|
|
134
|
+
class="text-base-500 dark:text-base-400 text-xs font-semibold tracking-wider uppercase"
|
|
135
|
+
>Interested</span
|
|
136
|
+
>
|
|
137
|
+
</p>
|
|
138
|
+
<div class="flex items-center">
|
|
139
|
+
<div class="flex flex-wrap -space-y-2 -space-x-4 pr-4">
|
|
140
|
+
{#each interestedDisplay as person (person.did)}
|
|
141
|
+
<div
|
|
142
|
+
animate:flip={{ duration: 300 }}
|
|
143
|
+
in:scale={{ duration: 300, start: 0.5 }}
|
|
144
|
+
out:scale={{ duration: 200, start: 0.5 }}
|
|
145
|
+
>
|
|
146
|
+
<FoxAvatar
|
|
147
|
+
src={thumbnail(person.avatar)}
|
|
148
|
+
alt={person.name}
|
|
149
|
+
class="border-base-100 dark:border-base-900 size-12 border-2"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
{/each}
|
|
153
|
+
{#if interestedOverflow > 0}
|
|
154
|
+
<span
|
|
155
|
+
class="bg-base-200 dark:bg-base-800 text-base-950 dark:text-base-100 border-base-100 dark:border-base-900 z-10 inline-flex size-12 items-center justify-center rounded-full border-2 text-sm font-semibold"
|
|
156
|
+
>
|
|
157
|
+
+{interestedOverflow}
|
|
158
|
+
</span>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</button>
|
|
163
|
+
{/if}
|
|
164
|
+
</div>
|
|
165
|
+
{/if}
|
|
166
|
+
|
|
167
|
+
<Modal
|
|
168
|
+
bind:open={modalOpen}
|
|
169
|
+
closeButton
|
|
170
|
+
onOpenAutoFocus={(e: Event) => e.preventDefault()}
|
|
171
|
+
class="p-0"
|
|
172
|
+
>
|
|
173
|
+
<p class="text-base-900 dark:text-base-50 px-4 pt-4 text-lg font-semibold">
|
|
174
|
+
{modalTitle}
|
|
175
|
+
<span class="text-base-500 dark:text-base-400 text-sm font-normal">
|
|
176
|
+
({modalAttendees.length})
|
|
177
|
+
</span>
|
|
178
|
+
</p>
|
|
179
|
+
<div
|
|
180
|
+
class="dark:bg-base-900/50 bg-base-200/30 mx-4 mb-4 max-h-80 space-y-1 overflow-y-auto rounded-xl p-2"
|
|
181
|
+
>
|
|
182
|
+
{#each modalAttendees as person (person.did)}
|
|
183
|
+
<a
|
|
184
|
+
href={person.url}
|
|
185
|
+
target={person.url?.startsWith('/') ? undefined : '_blank'}
|
|
186
|
+
rel={person.url?.startsWith('/') ? undefined : 'noopener noreferrer'}
|
|
187
|
+
class="hover:bg-base-200 dark:hover:bg-base-900 flex items-center gap-3 rounded-xl px-2 py-2 transition-colors"
|
|
188
|
+
>
|
|
189
|
+
<FoxAvatar src={thumbnail(person.avatar)} alt={person.name} class="size-10 shrink-0" />
|
|
190
|
+
<div class="min-w-0">
|
|
191
|
+
<p class="text-base-900 dark:text-base-50 truncate text-sm font-medium">
|
|
192
|
+
{person.name}
|
|
193
|
+
</p>
|
|
194
|
+
{#if person.handle}
|
|
195
|
+
<p class="text-base-500 dark:text-base-400 truncate text-xs">
|
|
196
|
+
@{person.handle}
|
|
197
|
+
</p>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
</a>
|
|
201
|
+
{/each}
|
|
202
|
+
</div>
|
|
203
|
+
</Modal>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AttendeeInfo } from './contrail.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
going?: AttendeeInfo[];
|
|
4
|
+
interested?: AttendeeInfo[];
|
|
5
|
+
goingCount?: number;
|
|
6
|
+
interestedCount?: number;
|
|
7
|
+
};
|
|
8
|
+
declare const EventAttendees: import("svelte").Component<$$ComponentProps, {
|
|
9
|
+
addAttendee: (attendee: AttendeeInfo) => void;
|
|
10
|
+
removeAttendee: (did: string) => void;
|
|
11
|
+
}, "">;
|
|
12
|
+
type EventAttendees = ReturnType<typeof EventAttendees>;
|
|
13
|
+
export default EventAttendees;
|