@axium/calendar 0.2.0 → 0.3.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/db.json +10 -10
- package/dist/client.d.ts +5 -1
- package/dist/common.d.ts +11 -0
- package/dist/common.js +40 -5
- package/dist/server.d.ts +1 -0
- package/dist/server.js +4 -1
- package/lib/Event.svelte +188 -0
- package/lib/index.ts +1 -0
- package/package.json +2 -1
- package/routes/calendar/+page.svelte +76 -151
package/db.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "../server/schemas/db.json",
|
|
3
|
-
"format":
|
|
3
|
+
"format": 1,
|
|
4
4
|
"versions": [
|
|
5
5
|
{
|
|
6
6
|
"delta": false,
|
|
@@ -58,15 +58,15 @@
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
-
"indexes":
|
|
62
|
-
"calendars:userId",
|
|
63
|
-
"acl.calendars:userId",
|
|
64
|
-
"acl.calendars:itemId",
|
|
65
|
-
"events:calId",
|
|
66
|
-
"events:recurrenceId",
|
|
67
|
-
"attendees:eventId",
|
|
68
|
-
"attendees:userId"
|
|
69
|
-
|
|
61
|
+
"indexes": {
|
|
62
|
+
"calendars_userId": { "on": "calendars", "columns": ["userId"] },
|
|
63
|
+
"acl_calendars_userId": { "on": "acl.calendars", "columns": ["userId"] },
|
|
64
|
+
"acl_calendars_itemId": { "on": "acl.calendars", "columns": ["itemId"] },
|
|
65
|
+
"events_calId": { "on": "events", "columns": ["calId"] },
|
|
66
|
+
"events_recurrenceId": { "on": "events", "columns": ["recurrenceId"] },
|
|
67
|
+
"attendees_eventId": { "on": "attendees", "columns": ["eventId"] },
|
|
68
|
+
"attendees_userId": { "on": "attendees", "columns": ["userId"] }
|
|
69
|
+
}
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"delta": true,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { Calendar, Event, EventData, EventFilter } from './common.js';
|
|
1
|
+
import type { AttendeeInit, Calendar, Event, EventData, EventFilter } from './common.js';
|
|
2
2
|
export declare function getEvents(calendars: Calendar[], filter: EventFilter): Promise<Event[]>;
|
|
3
3
|
export interface EventInitFormData extends Record<Exclude<keyof EventData, 'attendees' | 'recurrenceExcludes'>, string> {
|
|
4
4
|
}
|
|
5
|
+
export interface EventInitProp extends EventData {
|
|
6
|
+
attendees: AttendeeInit[];
|
|
7
|
+
calendar?: Calendar;
|
|
8
|
+
}
|
package/dist/common.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type UserInternal } from '@axium/core';
|
|
2
2
|
import type { WithRequired } from 'utilium';
|
|
3
3
|
import * as z from 'zod';
|
|
4
|
+
export declare function withOrdinalSuffix(val: number): string;
|
|
5
|
+
export declare function toRRuleDate(date: Date): Date;
|
|
6
|
+
export declare function fromRRuleDate(d: Date): Date;
|
|
4
7
|
export declare function dayOfYear(date: Date): number;
|
|
5
8
|
/**
|
|
6
9
|
*
|
|
@@ -10,6 +13,8 @@ export declare function dayOfYear(date: Date): number;
|
|
|
10
13
|
*/
|
|
11
14
|
export declare function weekOfYear(date: Date, baseOnWeeks?: boolean): number;
|
|
12
15
|
export declare function weekDaysFor(date: Date): Date[];
|
|
16
|
+
export declare function longWeekDay(date: Date): string;
|
|
17
|
+
export declare function weekDayOfMonth(date: Date): string;
|
|
13
18
|
/**
|
|
14
19
|
* Converts a date to the format expected by `<input type="datetime-local">`
|
|
15
20
|
*/
|
|
@@ -472,6 +477,12 @@ declare module '@axium/core/api' {
|
|
|
472
477
|
interface $API extends CalendarAPI {
|
|
473
478
|
}
|
|
474
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* Convert a `Date` to an iCalendar datetime
|
|
482
|
+
*/
|
|
483
|
+
export declare function toDateTime(date: Date): string;
|
|
484
|
+
/** e.g. `FR`, `SA` */
|
|
485
|
+
export declare function toByDay(date: Date): string;
|
|
475
486
|
export declare function eventToICS(event: Event): string;
|
|
476
487
|
export declare function eventFromICS(ics: string): Event;
|
|
477
488
|
export {};
|
package/dist/common.js
CHANGED
|
@@ -2,6 +2,22 @@ import { $API, AccessControl, pickPermissions } from '@axium/core';
|
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
import $pkg from '../package.json' with { type: 'json' };
|
|
4
4
|
import { Color } from '@axium/core/color';
|
|
5
|
+
export function withOrdinalSuffix(val) {
|
|
6
|
+
const tens = val % 10, hundreds = val % 100;
|
|
7
|
+
if (tens == 1 && hundreds != 11)
|
|
8
|
+
return val + 'st';
|
|
9
|
+
if (tens == 2 && hundreds != 12)
|
|
10
|
+
return val + 'nd';
|
|
11
|
+
if (tens == 3 && hundreds != 13)
|
|
12
|
+
return val + 'rd';
|
|
13
|
+
return val + 'th';
|
|
14
|
+
}
|
|
15
|
+
export function toRRuleDate(date) {
|
|
16
|
+
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes()));
|
|
17
|
+
}
|
|
18
|
+
export function fromRRuleDate(d) {
|
|
19
|
+
return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
|
|
20
|
+
}
|
|
5
21
|
export function dayOfYear(date) {
|
|
6
22
|
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
7
23
|
return Math.round((date.getTime() - yearStart.getTime()) / 86400000 + 1);
|
|
@@ -27,6 +43,13 @@ export function weekDaysFor(date) {
|
|
|
27
43
|
}
|
|
28
44
|
return days;
|
|
29
45
|
}
|
|
46
|
+
export function longWeekDay(date) {
|
|
47
|
+
return date.toLocaleString('default', { weekday: 'long' });
|
|
48
|
+
}
|
|
49
|
+
export function weekDayOfMonth(date) {
|
|
50
|
+
const weekOfMonth = Math.ceil(date.getDate() / 7);
|
|
51
|
+
return `${withOrdinalSuffix(weekOfMonth)} ${longWeekDay(date)}`;
|
|
52
|
+
}
|
|
30
53
|
/**
|
|
31
54
|
* Converts a date to the format expected by `<input type="datetime-local">`
|
|
32
55
|
*/
|
|
@@ -154,19 +177,31 @@ const CalendarAPI = {
|
|
|
154
177
|
},
|
|
155
178
|
};
|
|
156
179
|
Object.assign($API, CalendarAPI);
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Convert a `Date` to an iCalendar datetime
|
|
182
|
+
*/
|
|
183
|
+
export function toDateTime(date) {
|
|
184
|
+
return date
|
|
185
|
+
.toISOString()
|
|
186
|
+
.replaceAll('-', '')
|
|
187
|
+
.replaceAll(':', '')
|
|
188
|
+
.replace(/\.\d+Z$/, 'Z');
|
|
189
|
+
}
|
|
190
|
+
/** e.g. `FR`, `SA` */
|
|
191
|
+
export function toByDay(date) {
|
|
192
|
+
return date.toLocaleString('en', { weekday: 'short' }).slice(0, 2).toUpperCase();
|
|
159
193
|
}
|
|
160
194
|
export function eventToICS(event) {
|
|
161
195
|
const lines = [
|
|
162
196
|
'BEGIN:VCALENDAR',
|
|
163
197
|
'VERSION:2.0',
|
|
164
198
|
`PRODID:-//Axium//Calendar ${$pkg.version}//EN`,
|
|
199
|
+
'CALSCALE:GREGORIAN',
|
|
165
200
|
'BEGIN:VEVENT',
|
|
166
201
|
'UID:' + event.id,
|
|
167
|
-
'DTSTAMP:' +
|
|
168
|
-
'DTSTART:' +
|
|
169
|
-
'DTEND:' +
|
|
202
|
+
'DTSTAMP:' + toDateTime(new Date()),
|
|
203
|
+
'DTSTART:' + toDateTime(event.start),
|
|
204
|
+
'DTEND:' + toDateTime(event.end),
|
|
170
205
|
'SUMMARY:' + event.summary,
|
|
171
206
|
];
|
|
172
207
|
if (event.description)
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -87,7 +87,10 @@ addRoute({
|
|
|
87
87
|
.selectAll()
|
|
88
88
|
.select(withAttendees)
|
|
89
89
|
.where('calId', '=', id)
|
|
90
|
-
.where(
|
|
90
|
+
.where(eb => eb.or([
|
|
91
|
+
sql `(${sql.ref('start')}, ${sql.ref('end')}) OVERLAPS (${sql.val(filter.start)}, ${sql.val(filter.end)})`,
|
|
92
|
+
eb.and([eb('recurrence', 'is not', null), eb('recurrence', '!=', '')]), // @todo try to figure out a way to only select events we know recur during the filter
|
|
93
|
+
]))
|
|
91
94
|
.limit(1000)
|
|
92
95
|
.execute()
|
|
93
96
|
.then(result => result.map(withEncoded))
|
package/lib/Event.svelte
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EventInitProp } from '@axium/calendar/client';
|
|
3
|
+
import type { Event } from '@axium/calendar/common';
|
|
4
|
+
import { eventToICS, formatEventTimes, toRRuleDate } from '@axium/calendar/common';
|
|
5
|
+
import { contextMenu } from '@axium/client/attachments';
|
|
6
|
+
import { Icon, Popover } from '@axium/client/components';
|
|
7
|
+
import { colorHashHex, decodeColor, encodeColor } from '@axium/core/color';
|
|
8
|
+
import { rrulestr } from 'rrule';
|
|
9
|
+
import { download } from 'utilium/dom.js';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
event,
|
|
13
|
+
eventEditId = $bindable(),
|
|
14
|
+
eventEditCalId = $bindable(),
|
|
15
|
+
eventInit = $bindable(),
|
|
16
|
+
initData = event,
|
|
17
|
+
}: {
|
|
18
|
+
event: Event;
|
|
19
|
+
eventInit: EventInitProp;
|
|
20
|
+
eventEditId?: string;
|
|
21
|
+
eventEditCalId?: string;
|
|
22
|
+
initData?: Event;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const id = $props.id();
|
|
26
|
+
|
|
27
|
+
const recurrence = $derived(
|
|
28
|
+
!event.recurrence ? '' : rrulestr('RRULE:' + event.recurrence, { dtstart: toRRuleDate(event.start) }).toText()
|
|
29
|
+
);
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<Popover {id} onclick={e => e.stopPropagation()}>
|
|
33
|
+
{#snippet toggle()}
|
|
34
|
+
{@const start = event.start.getHours() * 60 + event.start.getMinutes()}
|
|
35
|
+
{@const end = event.end.getHours() * 60 + event.end.getMinutes()}
|
|
36
|
+
<div
|
|
37
|
+
class="Event"
|
|
38
|
+
style:top="{start / 14.4}%"
|
|
39
|
+
style:height="{(end - start) / 14.4}%"
|
|
40
|
+
style="--event-color:{decodeColor(
|
|
41
|
+
event.color || event.calendar!.color || encodeColor(colorHashHex(event.calendar!.name), true)
|
|
42
|
+
)}"
|
|
43
|
+
{@attach contextMenu(
|
|
44
|
+
{
|
|
45
|
+
i: 'pencil',
|
|
46
|
+
text: 'Edit',
|
|
47
|
+
action: () => {
|
|
48
|
+
eventEditId = event.id;
|
|
49
|
+
eventEditCalId = event.calId;
|
|
50
|
+
eventInit = initData;
|
|
51
|
+
document.querySelector<HTMLDialogElement>('#event-init')!.showModal();
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
i: 'file-export',
|
|
56
|
+
text: 'Export .ics',
|
|
57
|
+
action: () => download(event.summary + '.ics', eventToICS(event)),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
i: 'trash-can',
|
|
61
|
+
text: 'Delete',
|
|
62
|
+
action: () => {
|
|
63
|
+
eventEditId = event.id;
|
|
64
|
+
document.querySelector<HTMLDialogElement>('#event-delete')!.showModal();
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
<span>{event.summary}</span>
|
|
70
|
+
<span class="subtle">{formatEventTimes(event)}</span>
|
|
71
|
+
</div>
|
|
72
|
+
{/snippet}
|
|
73
|
+
|
|
74
|
+
<div class="event-actions">
|
|
75
|
+
<button
|
|
76
|
+
class="reset"
|
|
77
|
+
onclick={() => {
|
|
78
|
+
eventEditId = event.id;
|
|
79
|
+
eventEditCalId = event.calId;
|
|
80
|
+
eventInit = initData;
|
|
81
|
+
}}
|
|
82
|
+
command="show-modal"
|
|
83
|
+
commandfor="event-init"><Icon i="pencil" /></button
|
|
84
|
+
>
|
|
85
|
+
<button class="reset" onclick={() => download(event.summary + '.ics', eventToICS(event))}><Icon i="file-export" /></button>
|
|
86
|
+
<button class="reset" onclick={() => (eventEditId = event.id)} command="show-modal" commandfor="event-delete"
|
|
87
|
+
><Icon i="trash-can" /></button
|
|
88
|
+
>
|
|
89
|
+
<button class="reset" command="hide-popover" commandfor={id}><Icon i="xmark" /></button>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<h3>{event.summary}</h3>
|
|
93
|
+
|
|
94
|
+
<div>
|
|
95
|
+
<Icon i="clock" />
|
|
96
|
+
<span>
|
|
97
|
+
{#if event.start.getDate() == event.end.getDate()}
|
|
98
|
+
{event.start.toLocaleDateString()}, {formatEventTimes(event)}
|
|
99
|
+
{:else if event.isAllDay}
|
|
100
|
+
{event.start.toLocaleDateString()} - {event.end.toLocaleDateString()}
|
|
101
|
+
{:else}
|
|
102
|
+
{event.start.toLocaleString()} - {event.end.toLocaleString()}
|
|
103
|
+
{/if}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{#if event.recurrence}
|
|
108
|
+
<div>
|
|
109
|
+
<Icon i="arrows-rotate" />
|
|
110
|
+
<span>{recurrence}</span>
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
113
|
+
|
|
114
|
+
{#if event.location}
|
|
115
|
+
<div>
|
|
116
|
+
<Icon i="location-dot" />
|
|
117
|
+
<span>{event.location}</span>
|
|
118
|
+
</div>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<Icon i="calendar" />
|
|
123
|
+
<span>
|
|
124
|
+
{#if event.calendar}
|
|
125
|
+
{event.calendar.name}
|
|
126
|
+
{:else}
|
|
127
|
+
<i>Unknown Calendar</i>
|
|
128
|
+
{/if}
|
|
129
|
+
</span>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{#if event.attendees.length}
|
|
133
|
+
<div class="attendees-container">
|
|
134
|
+
<Icon i="user-group" />
|
|
135
|
+
<div class="attendees">
|
|
136
|
+
{#each event.attendees ?? [] as attendee (attendee.email)}
|
|
137
|
+
<div class="attendee">{attendee.email}</div>
|
|
138
|
+
{/each}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/if}
|
|
142
|
+
|
|
143
|
+
{#if event.description}
|
|
144
|
+
<div class="description">
|
|
145
|
+
<Icon i="block-quote" />
|
|
146
|
+
<span>{event.description}</span>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</Popover>
|
|
150
|
+
|
|
151
|
+
<style>
|
|
152
|
+
.Event {
|
|
153
|
+
width: calc(100% - 0.5em);
|
|
154
|
+
position: absolute;
|
|
155
|
+
border-radius: 0.5em;
|
|
156
|
+
padding: 0.25em;
|
|
157
|
+
background-color: var(--event-color, var(--bg-alt));
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
align-items: flex-start;
|
|
161
|
+
justify-content: flex-start;
|
|
162
|
+
container-type: size;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
|
|
165
|
+
@container (height < 2.5em) {
|
|
166
|
+
.subtle {
|
|
167
|
+
display: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
:global(& + :popover-open) {
|
|
172
|
+
gap: 0.75em;
|
|
173
|
+
padding: 1em;
|
|
174
|
+
|
|
175
|
+
.event-actions {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: flex-end;
|
|
179
|
+
gap: 0.25em;
|
|
180
|
+
|
|
181
|
+
button {
|
|
182
|
+
padding: 0.5em;
|
|
183
|
+
border-radius: 0.5em;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
</style>
|
package/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/calendar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"description": "Calendar for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"utilium": "^2.7.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"rrule": "^2.8.1",
|
|
45
46
|
"zod": "^4.0.5"
|
|
46
47
|
},
|
|
47
48
|
"axium": {
|
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getEvents, type EventInitFormData } from '@axium/calendar/client';
|
|
3
|
-
import type {
|
|
4
|
-
import {
|
|
2
|
+
import { getEvents, type EventInitFormData, type EventInitProp } from '@axium/calendar/client';
|
|
3
|
+
import type { Event } from '@axium/calendar/common';
|
|
4
|
+
import {
|
|
5
|
+
dateToInputValue,
|
|
6
|
+
fromRRuleDate,
|
|
7
|
+
getCalPermissionsInfo,
|
|
8
|
+
longWeekDay,
|
|
9
|
+
toByDay,
|
|
10
|
+
toRRuleDate,
|
|
11
|
+
weekDayOfMonth,
|
|
12
|
+
weekDaysFor,
|
|
13
|
+
withOrdinalSuffix,
|
|
14
|
+
} from '@axium/calendar/common';
|
|
5
15
|
import * as Cal from '@axium/calendar/components';
|
|
6
16
|
import { contextMenu, dynamicRows } from '@axium/client/attachments';
|
|
7
17
|
import { AccessControlDialog, ColorPicker, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
|
|
8
18
|
import { fetchAPI } from '@axium/client/requests';
|
|
9
|
-
import { colorHashHex, encodeColor
|
|
19
|
+
import { colorHashHex, encodeColor } from '@axium/core/color';
|
|
20
|
+
import { rrulestr } from 'rrule';
|
|
10
21
|
import { SvelteDate } from 'svelte/reactivity';
|
|
11
22
|
import { _throw } from 'utilium';
|
|
12
|
-
import z from 'zod';
|
|
23
|
+
import * as z from 'zod';
|
|
24
|
+
|
|
13
25
|
const { data } = $props();
|
|
14
26
|
|
|
15
27
|
const { user } = data.session;
|
|
16
28
|
|
|
17
29
|
const now = new SvelteDate();
|
|
30
|
+
now.setMilliseconds(0);
|
|
18
31
|
setInterval(() => now.setTime(Date.now()), 60_000);
|
|
19
32
|
const today = $derived(new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0));
|
|
33
|
+
const defaultStart = $derived(
|
|
34
|
+
new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), Math.round(now.getMinutes() / 30) * 30, 0, 0)
|
|
35
|
+
);
|
|
20
36
|
|
|
21
37
|
let start = new SvelteDate(data.filter.start);
|
|
22
38
|
let end = new SvelteDate(data.filter.end);
|
|
@@ -33,30 +49,36 @@
|
|
|
33
49
|
const span = $state('week');
|
|
34
50
|
const spanDays = $derived(span == 'week' ? 7 : _throw('Invalid span value'));
|
|
35
51
|
const weekDays = $derived(weekDaysFor(start));
|
|
36
|
-
const eventsForWeekDays = $derived(
|
|
37
|
-
Object.groupBy(
|
|
38
|
-
events.filter(
|
|
39
|
-
e => e.start < new Date(weekDays[6].getFullYear(), weekDays[6].getMonth(), weekDays[6].getDate() + 1) && e.end > weekDays[0]
|
|
40
|
-
),
|
|
41
|
-
ev => ev?.start.getDay()
|
|
42
|
-
)
|
|
43
|
-
);
|
|
44
52
|
|
|
45
53
|
let dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
46
54
|
|
|
47
|
-
const defaultEventInit = {
|
|
55
|
+
const defaultEventInit = $derived<any>({
|
|
48
56
|
attendees: [],
|
|
49
57
|
recurrenceExcludes: [],
|
|
50
58
|
recurrenceId: null,
|
|
51
59
|
calId: calendars[0]?.id,
|
|
52
|
-
|
|
60
|
+
start: new Date(defaultStart),
|
|
61
|
+
end: new Date(defaultStart.getTime() + 3600_000),
|
|
62
|
+
});
|
|
53
63
|
|
|
54
|
-
let eventInit = $state<
|
|
64
|
+
let eventInit = $state<EventInitProp>(defaultEventInit),
|
|
55
65
|
eventInitStart = $derived(dateToInputValue(eventInit.start)),
|
|
56
66
|
eventInitEnd = $derived(dateToInputValue(eventInit.end)),
|
|
57
67
|
eventEditId = $state<string>(),
|
|
58
68
|
eventEditCalId = $state<string>();
|
|
59
69
|
|
|
70
|
+
const recurringEvents = $derived(
|
|
71
|
+
events
|
|
72
|
+
.filter(ev => ev.recurrence)
|
|
73
|
+
.map(ev => {
|
|
74
|
+
const rule = rrulestr('RRULE:' + ev.recurrence, { dtstart: toRRuleDate(ev.start) });
|
|
75
|
+
const recurrences = rule
|
|
76
|
+
.between(toRRuleDate(new Date(start.getTime())), toRRuleDate(new Date(end.getTime())), true)
|
|
77
|
+
.map(fromRRuleDate);
|
|
78
|
+
return { ...ev, rule, recurrences };
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
60
82
|
const defaultEventColor = $derived((eventInit.calendar || calendars[0])?.color || encodeColor(colorHashHex(user.name)));
|
|
61
83
|
</script>
|
|
62
84
|
|
|
@@ -162,6 +184,14 @@
|
|
|
162
184
|
<span class="hour empty"></span>
|
|
163
185
|
</div>
|
|
164
186
|
{#if span == 'week'}
|
|
187
|
+
{@const eventsForWeekDays = Object.groupBy(
|
|
188
|
+
events.filter(
|
|
189
|
+
e =>
|
|
190
|
+
e.start < new Date(weekDays[6].getFullYear(), weekDays[6].getMonth(), weekDays[6].getDate() + 1) &&
|
|
191
|
+
e.end > weekDays[0]
|
|
192
|
+
),
|
|
193
|
+
ev => ev.start.getDay()
|
|
194
|
+
)}
|
|
165
195
|
<div class="cal-content week">
|
|
166
196
|
{#each weekDays as day, i}
|
|
167
197
|
<div class="day">
|
|
@@ -172,98 +202,13 @@
|
|
|
172
202
|
|
|
173
203
|
<div class="day-content">
|
|
174
204
|
{#each eventsForWeekDays[i] ?? [] as event}
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
style:height="{(end - start) / 14.4}%"
|
|
183
|
-
style="--event-color:{decodeColor(
|
|
184
|
-
event.color ||
|
|
185
|
-
event.calendar!.color ||
|
|
186
|
-
encodeColor(colorHashHex(event.calendar!.name), true)
|
|
187
|
-
)}"
|
|
188
|
-
>
|
|
189
|
-
<span>{event.summary}</span>
|
|
190
|
-
<span class="subtle">{formatEventTimes(event)}</span>
|
|
191
|
-
</div>
|
|
192
|
-
{/snippet}
|
|
193
|
-
|
|
194
|
-
<div class="event-actions">
|
|
195
|
-
<button
|
|
196
|
-
class="reset"
|
|
197
|
-
onclick={() => {
|
|
198
|
-
eventEditId = event.id;
|
|
199
|
-
eventEditCalId = event.calId;
|
|
200
|
-
eventInit = event;
|
|
201
|
-
}}
|
|
202
|
-
command="show-modal"
|
|
203
|
-
commandfor="event-init"><Icon i="pencil" /></button
|
|
204
|
-
>
|
|
205
|
-
<button
|
|
206
|
-
class="reset"
|
|
207
|
-
onclick={() => (eventEditId = event.id)}
|
|
208
|
-
command="show-modal"
|
|
209
|
-
commandfor="event-delete"><Icon i="trash-can" /></button
|
|
210
|
-
>
|
|
211
|
-
<button class="reset" command="hide-popover" commandfor="event-popover:{event.id}"
|
|
212
|
-
><Icon i="xmark" /></button
|
|
213
|
-
>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<h3>{event.summary}</h3>
|
|
217
|
-
|
|
218
|
-
<div>
|
|
219
|
-
<Icon i="clock" />
|
|
220
|
-
<span>
|
|
221
|
-
{#if event.start.getDate() == event.end.getDate()}
|
|
222
|
-
{event.start.toLocaleDateString()}, {formatEventTimes(event)}
|
|
223
|
-
{:else if event.isAllDay}
|
|
224
|
-
{event.start.toLocaleDateString()} - {event.end.toLocaleDateString()}
|
|
225
|
-
{:else}
|
|
226
|
-
{event.start.toLocaleString()} - {event.end.toLocaleString()}
|
|
227
|
-
{/if}
|
|
228
|
-
</span>
|
|
229
|
-
</div>
|
|
230
|
-
|
|
231
|
-
{#if event.location}
|
|
232
|
-
<div>
|
|
233
|
-
<Icon i="location-dot" />
|
|
234
|
-
<span>{event.location}</span>
|
|
235
|
-
</div>
|
|
236
|
-
{/if}
|
|
237
|
-
|
|
238
|
-
<div>
|
|
239
|
-
<Icon i="calendar" />
|
|
240
|
-
<span>
|
|
241
|
-
{#if event.calendar}
|
|
242
|
-
{event.calendar.name}
|
|
243
|
-
{:else}
|
|
244
|
-
<i>Unknown Calendar</i>
|
|
245
|
-
{/if}
|
|
246
|
-
</span>
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
|
-
{#if event.attendees.length}
|
|
250
|
-
<div class="attendees-container">
|
|
251
|
-
<Icon i="user-group" />
|
|
252
|
-
<div class="attendees">
|
|
253
|
-
{#each event.attendees ?? [] as attendee (attendee.email)}
|
|
254
|
-
<div class="attendee">{attendee.email}</div>
|
|
255
|
-
{/each}
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
{/if}
|
|
259
|
-
|
|
260
|
-
{#if event.description}
|
|
261
|
-
<div class="description">
|
|
262
|
-
<Icon i="block-quote" />
|
|
263
|
-
<span>{event.description}</span>
|
|
264
|
-
</div>
|
|
265
|
-
{/if}
|
|
266
|
-
</Popover>
|
|
205
|
+
<Cal.Event bind:eventEditId bind:eventEditCalId bind:eventInit {event} />
|
|
206
|
+
{/each}
|
|
207
|
+
|
|
208
|
+
{#each recurringEvents.flatMap(ev => ev.recurrences
|
|
209
|
+
.filter(r => r.getFullYear() == day.getFullYear() && r.getMonth() == day.getMonth() && r.getDate() == day.getDate())
|
|
210
|
+
.map( r => [ev, { ...ev, start: r, end: new Date(r.getTime() + ev.end.getTime() - ev.start.getTime()) }] )) as [initData, event]}
|
|
211
|
+
<Cal.Event bind:eventEditId bind:eventEditCalId bind:eventInit {event} {initData} />
|
|
267
212
|
{/each}
|
|
268
213
|
|
|
269
214
|
{#if today.getTime() == day.getTime()}
|
|
@@ -328,8 +273,22 @@
|
|
|
328
273
|
</label>
|
|
329
274
|
<label for="eventInit.isAllDay:checkbox">All day</label>
|
|
330
275
|
<div class="spacing"></div>
|
|
331
|
-
<select
|
|
332
|
-
|
|
276
|
+
<select name="recurrence" bind:value={eventInit.recurrence}>
|
|
277
|
+
<option value="">Does not repeat</option>
|
|
278
|
+
<option value="FREQ=DAILY">Every day</option>
|
|
279
|
+
<option value="FREQ=WEEKLY;BYDAY={toByDay(eventInit.start)}">
|
|
280
|
+
Every week on {longWeekDay(eventInit.start)}
|
|
281
|
+
</option>
|
|
282
|
+
<option value="FREQ=MONTHLY;BYDAY={Math.ceil(eventInit.start.getDate() / 7) + toByDay(eventInit.start)}"
|
|
283
|
+
>Every month on the {weekDayOfMonth(eventInit.start)}
|
|
284
|
+
</option>
|
|
285
|
+
<option value="FREQ=MONTHLY;BYMONTHDAY={eventInit.start.getDate()}">
|
|
286
|
+
Every month on the {withOrdinalSuffix(eventInit.start.getDate())}
|
|
287
|
+
</option>
|
|
288
|
+
<option value="FREQ=YEARLY;BYMONTH={eventInit.start.getMonth()};BYMONTHDAY={eventInit.start.getDate()}">
|
|
289
|
+
Every year on {eventInit.start.toLocaleDateString('default', { month: 'long', day: 'numeric' })}
|
|
290
|
+
</option>
|
|
291
|
+
<!-- @todo <option value="">Custom</option> -->
|
|
333
292
|
</select>
|
|
334
293
|
</div>
|
|
335
294
|
</div>
|
|
@@ -465,15 +424,18 @@
|
|
|
465
424
|
|
|
466
425
|
:global {
|
|
467
426
|
#event-init form {
|
|
468
|
-
width: 25em;
|
|
469
|
-
|
|
470
427
|
.event-time-options {
|
|
471
428
|
display: flex;
|
|
472
429
|
align-items: center;
|
|
473
430
|
gap: 0.5em;
|
|
474
431
|
|
|
475
|
-
.spacing
|
|
476
|
-
|
|
432
|
+
.spacing,
|
|
433
|
+
select {
|
|
434
|
+
flex: auto;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
label {
|
|
438
|
+
flex: none;
|
|
477
439
|
}
|
|
478
440
|
}
|
|
479
441
|
|
|
@@ -608,43 +570,6 @@
|
|
|
608
570
|
}
|
|
609
571
|
}
|
|
610
572
|
}
|
|
611
|
-
|
|
612
|
-
.event {
|
|
613
|
-
width: calc(100% - 0.5em);
|
|
614
|
-
position: absolute;
|
|
615
|
-
border-radius: 0.5em;
|
|
616
|
-
padding: 0.25em;
|
|
617
|
-
background-color: var(--event-color, var(--bg-alt));
|
|
618
|
-
display: flex;
|
|
619
|
-
flex-direction: column;
|
|
620
|
-
align-items: flex-start;
|
|
621
|
-
justify-content: flex-start;
|
|
622
|
-
container-type: size;
|
|
623
|
-
overflow: hidden;
|
|
624
|
-
|
|
625
|
-
@container (height < 2.5em) {
|
|
626
|
-
.subtle {
|
|
627
|
-
display: none;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
:global(& + :popover-open) {
|
|
632
|
-
gap: 0.75em;
|
|
633
|
-
padding: 1em;
|
|
634
|
-
|
|
635
|
-
.event-actions {
|
|
636
|
-
display: flex;
|
|
637
|
-
align-items: center;
|
|
638
|
-
justify-content: flex-end;
|
|
639
|
-
gap: 0.25em;
|
|
640
|
-
|
|
641
|
-
button {
|
|
642
|
-
padding: 0.5em;
|
|
643
|
-
border-radius: 0.5em;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
573
|
}
|
|
649
574
|
}
|
|
650
575
|
</style>
|