@axium/calendar 0.1.4 → 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 +25 -10
- package/dist/client.d.ts +5 -1
- package/dist/common.d.ts +27 -0
- package/dist/common.js +43 -5
- package/dist/server.d.ts +1 -0
- package/dist/server.js +33 -16
- package/lib/Event.svelte +188 -0
- package/lib/index.ts +1 -0
- package/package.json +4 -3
- package/routes/calendar/+page.svelte +89 -148
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,30 @@
|
|
|
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
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"delta": true,
|
|
73
|
+
"alter_tables": {
|
|
74
|
+
"calendars": {
|
|
75
|
+
"add_columns": {
|
|
76
|
+
"color": { "type": "integer" }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"events": {
|
|
80
|
+
"add_columns": {
|
|
81
|
+
"color": { "type": "integer" }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
70
85
|
}
|
|
71
86
|
],
|
|
72
87
|
"wipe": ["calendars", "events", "acl.calendars"],
|
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
|
*/
|
|
@@ -54,6 +59,7 @@ export declare const EventData: z.ZodObject<{
|
|
|
54
59
|
end: z.ZodCoercedDate<unknown>;
|
|
55
60
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
56
61
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
62
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
57
63
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
64
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
59
65
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -68,6 +74,7 @@ export declare const EventInit: z.ZodObject<{
|
|
|
68
74
|
end: z.ZodCoercedDate<unknown>;
|
|
69
75
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
70
76
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
77
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
71
78
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
72
79
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
73
80
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -89,6 +96,7 @@ export declare const Event: z.ZodObject<{
|
|
|
89
96
|
end: z.ZodCoercedDate<unknown>;
|
|
90
97
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
91
98
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
99
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
92
100
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
93
101
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
94
102
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -117,6 +125,7 @@ export declare const Calendar: z.ZodObject<{
|
|
|
117
125
|
id: z.ZodUUID;
|
|
118
126
|
userId: z.ZodUUID;
|
|
119
127
|
created: z.ZodCoercedDate<unknown>;
|
|
128
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
120
129
|
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
121
130
|
itemId: z.ZodUUID;
|
|
122
131
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -157,6 +166,7 @@ declare const CalendarAPI: {
|
|
|
157
166
|
id: z.ZodUUID;
|
|
158
167
|
userId: z.ZodUUID;
|
|
159
168
|
created: z.ZodCoercedDate<unknown>;
|
|
169
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
160
170
|
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
161
171
|
itemId: z.ZodUUID;
|
|
162
172
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -185,6 +195,7 @@ declare const CalendarAPI: {
|
|
|
185
195
|
id: z.ZodUUID;
|
|
186
196
|
userId: z.ZodUUID;
|
|
187
197
|
created: z.ZodCoercedDate<unknown>;
|
|
198
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
188
199
|
acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
189
200
|
itemId: z.ZodUUID;
|
|
190
201
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -215,6 +226,7 @@ declare const CalendarAPI: {
|
|
|
215
226
|
id: z.ZodUUID;
|
|
216
227
|
userId: z.ZodUUID;
|
|
217
228
|
created: z.ZodCoercedDate<unknown>;
|
|
229
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
218
230
|
acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
219
231
|
itemId: z.ZodUUID;
|
|
220
232
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -245,6 +257,7 @@ declare const CalendarAPI: {
|
|
|
245
257
|
id: z.ZodUUID;
|
|
246
258
|
userId: z.ZodUUID;
|
|
247
259
|
created: z.ZodCoercedDate<unknown>;
|
|
260
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
248
261
|
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
249
262
|
itemId: z.ZodUUID;
|
|
250
263
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -273,6 +286,7 @@ declare const CalendarAPI: {
|
|
|
273
286
|
id: z.ZodUUID;
|
|
274
287
|
userId: z.ZodUUID;
|
|
275
288
|
created: z.ZodCoercedDate<unknown>;
|
|
289
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
276
290
|
acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
277
291
|
itemId: z.ZodUUID;
|
|
278
292
|
userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -309,6 +323,7 @@ declare const CalendarAPI: {
|
|
|
309
323
|
end: z.ZodCoercedDate<unknown>;
|
|
310
324
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
311
325
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
326
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
312
327
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
313
328
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
314
329
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -331,6 +346,7 @@ declare const CalendarAPI: {
|
|
|
331
346
|
end: z.ZodCoercedDate<unknown>;
|
|
332
347
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
333
348
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
349
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
334
350
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
335
351
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
336
352
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -349,6 +365,7 @@ declare const CalendarAPI: {
|
|
|
349
365
|
end: z.ZodCoercedDate<unknown>;
|
|
350
366
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
351
367
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
368
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
352
369
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
353
370
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
354
371
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -373,6 +390,7 @@ declare const CalendarAPI: {
|
|
|
373
390
|
end: z.ZodCoercedDate<unknown>;
|
|
374
391
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
375
392
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
393
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
376
394
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
377
395
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
378
396
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -395,6 +413,7 @@ declare const CalendarAPI: {
|
|
|
395
413
|
end: z.ZodCoercedDate<unknown>;
|
|
396
414
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
397
415
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
416
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
398
417
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
399
418
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
400
419
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -413,6 +432,7 @@ declare const CalendarAPI: {
|
|
|
413
432
|
end: z.ZodCoercedDate<unknown>;
|
|
414
433
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
415
434
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
435
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
416
436
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
417
437
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
418
438
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -435,6 +455,7 @@ declare const CalendarAPI: {
|
|
|
435
455
|
end: z.ZodCoercedDate<unknown>;
|
|
436
456
|
isAllDay: z.ZodCoercedBoolean<unknown>;
|
|
437
457
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
458
|
+
color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
|
|
438
459
|
recurrence: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
439
460
|
recurrenceExcludes: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
440
461
|
recurrenceId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
|
|
@@ -456,6 +477,12 @@ declare module '@axium/core/api' {
|
|
|
456
477
|
interface $API extends CalendarAPI {
|
|
457
478
|
}
|
|
458
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;
|
|
459
486
|
export declare function eventToICS(event: Event): string;
|
|
460
487
|
export declare function eventFromICS(ics: string): Event;
|
|
461
488
|
export {};
|
package/dist/common.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
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
|
+
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
|
+
}
|
|
4
21
|
export function dayOfYear(date) {
|
|
5
22
|
const yearStart = new Date(date.getFullYear(), 0, 1);
|
|
6
23
|
return Math.round((date.getTime() - yearStart.getTime()) / 86400000 + 1);
|
|
@@ -26,6 +43,13 @@ export function weekDaysFor(date) {
|
|
|
26
43
|
}
|
|
27
44
|
return days;
|
|
28
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
|
+
}
|
|
29
53
|
/**
|
|
30
54
|
* Converts a date to the format expected by `<input type="datetime-local">`
|
|
31
55
|
*/
|
|
@@ -81,6 +105,7 @@ export const EventData = z.object({
|
|
|
81
105
|
end: z.coerce.date(),
|
|
82
106
|
isAllDay: z.coerce.boolean(),
|
|
83
107
|
description: z.string().max(2000).nullish(),
|
|
108
|
+
color: Color.nullish(),
|
|
84
109
|
// note: recurrences are not support yet
|
|
85
110
|
recurrence: z.string().max(1000).nullish(),
|
|
86
111
|
recurrenceExcludes: z.string().max(100).array().max(100).nullish(),
|
|
@@ -111,6 +136,7 @@ export const Calendar = CalendarInit.extend({
|
|
|
111
136
|
id: z.uuid(),
|
|
112
137
|
userId: z.uuid(),
|
|
113
138
|
created: z.coerce.date(),
|
|
139
|
+
color: Color.nullish(),
|
|
114
140
|
acl: AccessControl.array().optional(),
|
|
115
141
|
});
|
|
116
142
|
export function getCalPermissionsInfo(cal, user) {
|
|
@@ -151,19 +177,31 @@ const CalendarAPI = {
|
|
|
151
177
|
},
|
|
152
178
|
};
|
|
153
179
|
Object.assign($API, CalendarAPI);
|
|
154
|
-
|
|
155
|
-
|
|
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();
|
|
156
193
|
}
|
|
157
194
|
export function eventToICS(event) {
|
|
158
195
|
const lines = [
|
|
159
196
|
'BEGIN:VCALENDAR',
|
|
160
197
|
'VERSION:2.0',
|
|
161
198
|
`PRODID:-//Axium//Calendar ${$pkg.version}//EN`,
|
|
199
|
+
'CALSCALE:GREGORIAN',
|
|
162
200
|
'BEGIN:VEVENT',
|
|
163
201
|
'UID:' + event.id,
|
|
164
|
-
'DTSTAMP:' +
|
|
165
|
-
'DTSTART:' +
|
|
166
|
-
'DTEND:' +
|
|
202
|
+
'DTSTAMP:' + toDateTime(new Date()),
|
|
203
|
+
'DTSTART:' + toDateTime(event.start),
|
|
204
|
+
'DTEND:' + toDateTime(event.end),
|
|
167
205
|
'SUMMARY:' + event.summary,
|
|
168
206
|
];
|
|
169
207
|
if (event.description)
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -7,28 +7,37 @@ import { sql } from 'kysely';
|
|
|
7
7
|
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
8
8
|
import * as z from 'zod';
|
|
9
9
|
import { Attendee, CalendarInit, EventFilter, EventInit } from './common.js';
|
|
10
|
+
function withEncoded(value) {
|
|
11
|
+
return (value.color === undefined || value.color === null
|
|
12
|
+
? value
|
|
13
|
+
: Object.assign(value, { color: value.color.toString(16).padStart(8, '0') }));
|
|
14
|
+
}
|
|
15
|
+
function withDecoded(value) {
|
|
16
|
+
return (value.color === undefined || value.color === null ? value : Object.assign(value, { color: parseInt(value.color, 16) }));
|
|
17
|
+
}
|
|
10
18
|
addRoute({
|
|
11
19
|
path: '/api/users/:id/calendars',
|
|
12
20
|
params: { id: z.uuid() },
|
|
13
21
|
async GET(request, { id: userId }) {
|
|
14
22
|
const { user } = await checkAuthForUser(request, userId);
|
|
15
|
-
|
|
23
|
+
const result = await database
|
|
16
24
|
.selectFrom('calendars')
|
|
17
25
|
.selectAll()
|
|
18
26
|
.select(aclFrom('calendars'))
|
|
19
27
|
.where(userHasAccess('calendars', user))
|
|
20
28
|
.execute()
|
|
21
29
|
.catch(withError('Could not get calendars'));
|
|
30
|
+
return result.map(c => withEncoded(c));
|
|
22
31
|
},
|
|
23
32
|
async PUT(request, { id: userId }) {
|
|
24
33
|
const init = await parseBody(request, CalendarInit);
|
|
25
34
|
await checkAuthForUser(request, userId);
|
|
26
|
-
return await database
|
|
35
|
+
return withEncoded(await database
|
|
27
36
|
.insertInto('calendars')
|
|
28
37
|
.values({ ...init, userId })
|
|
29
38
|
.returningAll()
|
|
30
39
|
.executeTakeFirstOrThrow()
|
|
31
|
-
.catch(withError('Could not create calendar'));
|
|
40
|
+
.catch(withError('Could not create calendar')));
|
|
32
41
|
},
|
|
33
42
|
});
|
|
34
43
|
addRoute({
|
|
@@ -36,27 +45,27 @@ addRoute({
|
|
|
36
45
|
params: { id: z.uuid() },
|
|
37
46
|
async GET(request, { id }) {
|
|
38
47
|
const { item } = await authRequestForItem(request, 'calendars', id, { read: true });
|
|
39
|
-
return item;
|
|
48
|
+
return withEncoded(item);
|
|
40
49
|
},
|
|
41
50
|
async PATCH(request, { id }) {
|
|
42
51
|
const body = await parseBody(request, CalendarInit);
|
|
43
52
|
await authRequestForItem(request, 'calendars', id, { edit: true });
|
|
44
|
-
return await database
|
|
53
|
+
return withEncoded(await database
|
|
45
54
|
.updateTable('calendars')
|
|
46
55
|
.set(body)
|
|
47
56
|
.where('id', '=', id)
|
|
48
57
|
.returningAll()
|
|
49
58
|
.executeTakeFirstOrThrow()
|
|
50
|
-
.catch(withError('Could not update calendar'));
|
|
59
|
+
.catch(withError('Could not update calendar')));
|
|
51
60
|
},
|
|
52
61
|
async DELETE(request, { id }) {
|
|
53
62
|
await authRequestForItem(request, 'calendars', id, { manage: true });
|
|
54
|
-
return await database
|
|
63
|
+
return withEncoded(await database
|
|
55
64
|
.deleteFrom('calendars')
|
|
56
65
|
.where('id', '=', id)
|
|
57
66
|
.returningAll()
|
|
58
67
|
.executeTakeFirstOrThrow()
|
|
59
|
-
.catch(withError('Could not delete calendar'));
|
|
68
|
+
.catch(withError('Could not delete calendar')));
|
|
60
69
|
},
|
|
61
70
|
});
|
|
62
71
|
function withAttendees(eb) {
|
|
@@ -78,12 +87,16 @@ addRoute({
|
|
|
78
87
|
.selectAll()
|
|
79
88
|
.select(withAttendees)
|
|
80
89
|
.where('calId', '=', id)
|
|
81
|
-
.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
|
+
]))
|
|
82
94
|
.limit(1000)
|
|
83
95
|
.execute()
|
|
96
|
+
.then(result => result.map(withEncoded))
|
|
84
97
|
.catch(withError('Could not get events'));
|
|
85
98
|
for (const event of events)
|
|
86
|
-
event.calendar = calendar;
|
|
99
|
+
event.calendar = withEncoded(calendar);
|
|
87
100
|
return events;
|
|
88
101
|
},
|
|
89
102
|
async PUT(request, { id }) {
|
|
@@ -93,7 +106,7 @@ addRoute({
|
|
|
93
106
|
try {
|
|
94
107
|
const event = await tx
|
|
95
108
|
.insertInto('events')
|
|
96
|
-
.values({ ...init, calId: id })
|
|
109
|
+
.values({ ...withDecoded(init), calId: id })
|
|
97
110
|
.returningAll()
|
|
98
111
|
.executeTakeFirstOrThrow()
|
|
99
112
|
.catch(withError('Could not create event'));
|
|
@@ -105,7 +118,11 @@ addRoute({
|
|
|
105
118
|
.execute()
|
|
106
119
|
: []);
|
|
107
120
|
await tx.commit().execute();
|
|
108
|
-
return Object.assign(event, {
|
|
121
|
+
return Object.assign(event, {
|
|
122
|
+
attendees,
|
|
123
|
+
calendar: withEncoded(calendar),
|
|
124
|
+
color: event.color?.toString(16)?.padStart(8, '0'),
|
|
125
|
+
});
|
|
109
126
|
}
|
|
110
127
|
catch (e) {
|
|
111
128
|
await tx.rollback().execute();
|
|
@@ -128,7 +145,7 @@ addRoute({
|
|
|
128
145
|
.catch(withError('Event does not exist', 404));
|
|
129
146
|
if (event.calendar.userId != userId && event.attendees.every(a => a.userId != userId))
|
|
130
147
|
error(403, 'You do not have access to this event');
|
|
131
|
-
return event;
|
|
148
|
+
return withEncoded(event);
|
|
132
149
|
},
|
|
133
150
|
async PATCH(request, { id }) {
|
|
134
151
|
const { attendees: attendeesInit = [], sendEmails, recurrenceUpdateAll, ...init } = await parseBody(request, EventInit);
|
|
@@ -145,7 +162,7 @@ addRoute({
|
|
|
145
162
|
try {
|
|
146
163
|
const event = await tx
|
|
147
164
|
.updateTable('events')
|
|
148
|
-
.set(init)
|
|
165
|
+
.set(withDecoded(init))
|
|
149
166
|
.where('id', '=', id)
|
|
150
167
|
.returningAll()
|
|
151
168
|
.returning(withAttendees)
|
|
@@ -173,7 +190,7 @@ addRoute({
|
|
|
173
190
|
*/
|
|
174
191
|
}
|
|
175
192
|
await tx.commit().execute();
|
|
176
|
-
return event;
|
|
193
|
+
return withEncoded(event);
|
|
177
194
|
}
|
|
178
195
|
catch (e) {
|
|
179
196
|
await tx.rollback().execute();
|
|
@@ -198,7 +215,7 @@ addRoute({
|
|
|
198
215
|
.where('id', '=', id)
|
|
199
216
|
.executeTakeFirstOrThrow();
|
|
200
217
|
await tx.commit().execute();
|
|
201
|
-
return event;
|
|
218
|
+
return withEncoded(event);
|
|
202
219
|
}
|
|
203
220
|
catch (e) {
|
|
204
221
|
await tx.rollback().execute();
|
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": {
|
|
@@ -36,12 +36,13 @@
|
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@axium/client": ">=0.14.3",
|
|
39
|
-
"@axium/core": ">=0.
|
|
39
|
+
"@axium/core": ">=0.20.0",
|
|
40
40
|
"@axium/server": ">=0.36.0",
|
|
41
41
|
"@sveltejs/kit": "^2.27.3",
|
|
42
|
-
"utilium": "^2.
|
|
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,21 +1,38 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { getEvents, type EventInitFormData } from '@axium/calendar/client';
|
|
3
|
-
import type { Event
|
|
4
|
-
import {
|
|
5
|
-
|
|
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';
|
|
15
|
+
import * as Cal from '@axium/calendar/components';
|
|
6
16
|
import { contextMenu, dynamicRows } from '@axium/client/attachments';
|
|
7
|
-
import { AccessControlDialog, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
|
|
17
|
+
import { AccessControlDialog, ColorPicker, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
|
|
8
18
|
import { fetchAPI } from '@axium/client/requests';
|
|
19
|
+
import { colorHashHex, encodeColor } from '@axium/core/color';
|
|
20
|
+
import { rrulestr } from 'rrule';
|
|
9
21
|
import { SvelteDate } from 'svelte/reactivity';
|
|
10
22
|
import { _throw } from 'utilium';
|
|
11
|
-
import z from 'zod';
|
|
23
|
+
import * as z from 'zod';
|
|
24
|
+
|
|
12
25
|
const { data } = $props();
|
|
13
26
|
|
|
14
27
|
const { user } = data.session;
|
|
15
28
|
|
|
16
29
|
const now = new SvelteDate();
|
|
30
|
+
now.setMilliseconds(0);
|
|
17
31
|
setInterval(() => now.setTime(Date.now()), 60_000);
|
|
18
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
|
+
);
|
|
19
36
|
|
|
20
37
|
let start = new SvelteDate(data.filter.start);
|
|
21
38
|
let end = new SvelteDate(data.filter.end);
|
|
@@ -23,6 +40,7 @@
|
|
|
23
40
|
let events = $state<Event[]>([]);
|
|
24
41
|
|
|
25
42
|
$effect(() => {
|
|
43
|
+
for (const cal of calendars) cal.color ||= encodeColor(colorHashHex(cal.name));
|
|
26
44
|
getEvents(calendars, { start: new Date(start.getTime()), end: new Date(end.getTime()) }).then(e => (events = e));
|
|
27
45
|
});
|
|
28
46
|
|
|
@@ -31,27 +49,37 @@
|
|
|
31
49
|
const span = $state('week');
|
|
32
50
|
const spanDays = $derived(span == 'week' ? 7 : _throw('Invalid span value'));
|
|
33
51
|
const weekDays = $derived(weekDaysFor(start));
|
|
34
|
-
const eventsForWeekDays = $derived(
|
|
35
|
-
Object.groupBy(
|
|
36
|
-
events.filter(
|
|
37
|
-
e => e.start < new Date(weekDays[6].getFullYear(), weekDays[6].getMonth(), weekDays[6].getDate() + 1) && e.end > weekDays[0]
|
|
38
|
-
),
|
|
39
|
-
ev => ev?.start.getDay()
|
|
40
|
-
)
|
|
41
|
-
);
|
|
42
52
|
|
|
43
53
|
let dialogs = $state<Record<string, HTMLDialogElement>>({});
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const defaultEventInit = $derived<any>({
|
|
56
|
+
attendees: [],
|
|
57
|
+
recurrenceExcludes: [],
|
|
58
|
+
recurrenceId: null,
|
|
59
|
+
calId: calendars[0]?.id,
|
|
60
|
+
start: new Date(defaultStart),
|
|
61
|
+
end: new Date(defaultStart.getTime() + 3600_000),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let eventInit = $state<EventInitProp>(defaultEventInit),
|
|
51
65
|
eventInitStart = $derived(dateToInputValue(eventInit.start)),
|
|
52
66
|
eventInitEnd = $derived(dateToInputValue(eventInit.end)),
|
|
53
67
|
eventEditId = $state<string>(),
|
|
54
68
|
eventEditCalId = $state<string>();
|
|
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
|
+
|
|
82
|
+
const defaultEventColor = $derived((eventInit.calendar || calendars[0])?.color || encodeColor(colorHashHex(user.name)));
|
|
55
83
|
</script>
|
|
56
84
|
|
|
57
85
|
<svelte:head>
|
|
@@ -79,7 +107,7 @@
|
|
|
79
107
|
<span class="label">{weekDays[0].toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
|
|
80
108
|
</div>
|
|
81
109
|
<div id="cal-list">
|
|
82
|
-
<
|
|
110
|
+
<Cal.Select bind:start bind:end />
|
|
83
111
|
<div class="cal-list-header">
|
|
84
112
|
<h4>My Calendars</h4>
|
|
85
113
|
<button style:display="contents" command="show-modal" commandfor="add-calendar">
|
|
@@ -156,6 +184,14 @@
|
|
|
156
184
|
<span class="hour empty"></span>
|
|
157
185
|
</div>
|
|
158
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
|
+
)}
|
|
159
195
|
<div class="cal-content week">
|
|
160
196
|
{#each weekDays as day, i}
|
|
161
197
|
<div class="day">
|
|
@@ -166,89 +202,13 @@
|
|
|
166
202
|
|
|
167
203
|
<div class="day-content">
|
|
168
204
|
{#each eventsForWeekDays[i] ?? [] as event}
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
</div>
|
|
177
|
-
{/snippet}
|
|
178
|
-
|
|
179
|
-
<div class="event-actions">
|
|
180
|
-
<button
|
|
181
|
-
class="reset"
|
|
182
|
-
onclick={() => {
|
|
183
|
-
eventEditId = event.id;
|
|
184
|
-
eventEditCalId = event.calId;
|
|
185
|
-
eventInit = event;
|
|
186
|
-
}}
|
|
187
|
-
command="show-modal"
|
|
188
|
-
commandfor="event-init"><Icon i="pencil" /></button
|
|
189
|
-
>
|
|
190
|
-
<button
|
|
191
|
-
class="reset"
|
|
192
|
-
onclick={() => (eventEditId = event.id)}
|
|
193
|
-
command="show-modal"
|
|
194
|
-
commandfor="event-delete"><Icon i="trash-can" /></button
|
|
195
|
-
>
|
|
196
|
-
<button class="reset" command="hide-popover" commandfor="event-popover:{event.id}"
|
|
197
|
-
><Icon i="xmark" /></button
|
|
198
|
-
>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<h3>{event.summary}</h3>
|
|
202
|
-
|
|
203
|
-
<div>
|
|
204
|
-
<Icon i="clock" />
|
|
205
|
-
<span>
|
|
206
|
-
{#if event.start.getDate() == event.end.getDate()}
|
|
207
|
-
{event.start.toLocaleDateString()}, {formatEventTimes(event)}
|
|
208
|
-
{:else if event.isAllDay}
|
|
209
|
-
{event.start.toLocaleDateString()} - {event.end.toLocaleDateString()}
|
|
210
|
-
{:else}
|
|
211
|
-
{event.start.toLocaleString()} - {event.end.toLocaleString()}
|
|
212
|
-
{/if}
|
|
213
|
-
</span>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
{#if event.location}
|
|
217
|
-
<div>
|
|
218
|
-
<Icon i="location-dot" />
|
|
219
|
-
<span>{event.location}</span>
|
|
220
|
-
</div>
|
|
221
|
-
{/if}
|
|
222
|
-
|
|
223
|
-
<div>
|
|
224
|
-
<Icon i="calendar" />
|
|
225
|
-
<span>
|
|
226
|
-
{#if event.calendar}
|
|
227
|
-
{event.calendar.name}
|
|
228
|
-
{:else}
|
|
229
|
-
<i>Unknown Calendar</i>
|
|
230
|
-
{/if}
|
|
231
|
-
</span>
|
|
232
|
-
</div>
|
|
233
|
-
|
|
234
|
-
{#if event.attendees.length}
|
|
235
|
-
<div class="attendees-container">
|
|
236
|
-
<Icon i="user-group" />
|
|
237
|
-
<div class="attendees">
|
|
238
|
-
{#each event.attendees ?? [] as attendee (attendee.email)}
|
|
239
|
-
<div class="attendee">{attendee.email}</div>
|
|
240
|
-
{/each}
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
{/if}
|
|
244
|
-
|
|
245
|
-
{#if event.description}
|
|
246
|
-
<div class="description">
|
|
247
|
-
<Icon i="block-quote" />
|
|
248
|
-
<span>{event.description}</span>
|
|
249
|
-
</div>
|
|
250
|
-
{/if}
|
|
251
|
-
</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} />
|
|
252
212
|
{/each}
|
|
253
213
|
|
|
254
214
|
{#if today.getTime() == day.getTime()}
|
|
@@ -265,7 +225,7 @@
|
|
|
265
225
|
<FormDialog
|
|
266
226
|
id="event-init"
|
|
267
227
|
clearOnCancel
|
|
268
|
-
cancel={() => (eventInit =
|
|
228
|
+
cancel={() => (eventInit = defaultEventInit)}
|
|
269
229
|
submitText={eventEditId ? 'Update' : 'Create'}
|
|
270
230
|
submit={async (data: EventInitFormData) => {
|
|
271
231
|
Object.assign(eventInit, data);
|
|
@@ -313,8 +273,22 @@
|
|
|
313
273
|
</label>
|
|
314
274
|
<label for="eventInit.isAllDay:checkbox">All day</label>
|
|
315
275
|
<div class="spacing"></div>
|
|
316
|
-
<select
|
|
317
|
-
|
|
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> -->
|
|
318
292
|
</select>
|
|
319
293
|
</div>
|
|
320
294
|
</div>
|
|
@@ -327,6 +301,7 @@
|
|
|
327
301
|
<option value={cal.id}>{cal.name}</option>
|
|
328
302
|
{/each}
|
|
329
303
|
</select>
|
|
304
|
+
<ColorPicker bind:value={eventInit.color} defaultValue={defaultEventColor} />
|
|
330
305
|
</div>
|
|
331
306
|
|
|
332
307
|
<div class="attendees-container">
|
|
@@ -449,15 +424,18 @@
|
|
|
449
424
|
|
|
450
425
|
:global {
|
|
451
426
|
#event-init form {
|
|
452
|
-
width: 25em;
|
|
453
|
-
|
|
454
427
|
.event-time-options {
|
|
455
428
|
display: flex;
|
|
456
429
|
align-items: center;
|
|
457
430
|
gap: 0.5em;
|
|
458
431
|
|
|
459
|
-
.spacing
|
|
460
|
-
|
|
432
|
+
.spacing,
|
|
433
|
+
select {
|
|
434
|
+
flex: auto;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
label {
|
|
438
|
+
flex: none;
|
|
461
439
|
}
|
|
462
440
|
}
|
|
463
441
|
|
|
@@ -592,43 +570,6 @@
|
|
|
592
570
|
}
|
|
593
571
|
}
|
|
594
572
|
}
|
|
595
|
-
|
|
596
|
-
.event {
|
|
597
|
-
width: calc(100% - 0.5em);
|
|
598
|
-
position: absolute;
|
|
599
|
-
border-radius: 0.5em;
|
|
600
|
-
padding: 0.25em;
|
|
601
|
-
background-color: var(--bg-alt);
|
|
602
|
-
display: flex;
|
|
603
|
-
flex-direction: column;
|
|
604
|
-
align-items: flex-start;
|
|
605
|
-
justify-content: flex-start;
|
|
606
|
-
container-type: size;
|
|
607
|
-
overflow: hidden;
|
|
608
|
-
|
|
609
|
-
@container (height < 2.5em) {
|
|
610
|
-
.subtle {
|
|
611
|
-
display: none;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
:global(& + :popover-open) {
|
|
616
|
-
gap: 0.75em;
|
|
617
|
-
padding: 1em;
|
|
618
|
-
|
|
619
|
-
.event-actions {
|
|
620
|
-
display: flex;
|
|
621
|
-
align-items: center;
|
|
622
|
-
justify-content: flex-end;
|
|
623
|
-
gap: 0.25em;
|
|
624
|
-
|
|
625
|
-
button {
|
|
626
|
-
padding: 0.5em;
|
|
627
|
-
border-radius: 0.5em;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
573
|
}
|
|
633
574
|
}
|
|
634
575
|
</style>
|