@axium/calendar 0.3.3 → 0.4.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/common.d.ts CHANGED
@@ -117,15 +117,16 @@ export interface Event extends z.infer<typeof Event> {
117
117
  export declare function formatEventTimes(event: Event): string;
118
118
  export declare const CalendarInit: z.ZodObject<{
119
119
  name: z.ZodString;
120
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
120
121
  }, z.core.$strip>;
121
122
  export interface CalendarInit extends z.infer<typeof CalendarInit> {
122
123
  }
123
124
  export declare const Calendar: z.ZodObject<{
124
125
  name: z.ZodString;
126
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
125
127
  id: z.ZodUUID;
126
128
  userId: z.ZodUUID;
127
129
  created: z.ZodCoercedDate<unknown>;
128
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
129
130
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
130
131
  itemId: z.ZodUUID;
131
132
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -161,12 +162,13 @@ declare const CalendarAPI: {
161
162
  readonly 'users/:id/calendars': {
162
163
  readonly PUT: readonly [z.ZodObject<{
163
164
  name: z.ZodString;
165
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
164
166
  }, z.core.$strip>, z.ZodObject<{
165
167
  name: z.ZodString;
168
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
166
169
  id: z.ZodUUID;
167
170
  userId: z.ZodUUID;
168
171
  created: z.ZodCoercedDate<unknown>;
169
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
170
172
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
171
173
  itemId: z.ZodUUID;
172
174
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -192,10 +194,10 @@ declare const CalendarAPI: {
192
194
  }, z.core.$strip>];
193
195
  readonly GET: z.ZodArray<z.ZodObject<{
194
196
  name: z.ZodString;
197
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
195
198
  id: z.ZodUUID;
196
199
  userId: z.ZodUUID;
197
200
  created: z.ZodCoercedDate<unknown>;
198
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
199
201
  acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
200
202
  itemId: z.ZodUUID;
201
203
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -223,10 +225,10 @@ declare const CalendarAPI: {
223
225
  readonly 'calendars/:id': {
224
226
  readonly GET: z.ZodObject<{
225
227
  name: z.ZodString;
228
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
226
229
  id: z.ZodUUID;
227
230
  userId: z.ZodUUID;
228
231
  created: z.ZodCoercedDate<unknown>;
229
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
230
232
  acl: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
231
233
  itemId: z.ZodUUID;
232
234
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -252,12 +254,13 @@ declare const CalendarAPI: {
252
254
  }, z.core.$strip>;
253
255
  readonly PATCH: readonly [z.ZodObject<{
254
256
  name: z.ZodString;
257
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
255
258
  }, z.core.$strip>, z.ZodObject<{
256
259
  name: z.ZodString;
260
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
257
261
  id: z.ZodUUID;
258
262
  userId: z.ZodUUID;
259
263
  created: z.ZodCoercedDate<unknown>;
260
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
261
264
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
262
265
  itemId: z.ZodUUID;
263
266
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
@@ -283,10 +286,10 @@ declare const CalendarAPI: {
283
286
  }, z.core.$strip>];
284
287
  readonly DELETE: z.ZodObject<{
285
288
  name: z.ZodString;
289
+ color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
286
290
  id: z.ZodUUID;
287
291
  userId: z.ZodUUID;
288
292
  created: z.ZodCoercedDate<unknown>;
289
- color: z.ZodOptional<z.ZodNullable<z.ZodCustomStringFormat<"hex">>>;
290
293
  acl: z.ZodOptional<z.ZodArray<z.ZodObject<{
291
294
  itemId: z.ZodUUID;
292
295
  userId: z.ZodOptional<z.ZodNullable<z.ZodUUID>>;
package/dist/common.js CHANGED
@@ -131,12 +131,12 @@ export function formatEventTimes(event) {
131
131
  }
132
132
  export const CalendarInit = z.object({
133
133
  name: z.string(),
134
+ color: Color.nullish(),
134
135
  });
135
136
  export const Calendar = CalendarInit.extend({
136
137
  id: z.uuid(),
137
138
  userId: z.uuid(),
138
139
  created: z.coerce.date(),
139
- color: Color.nullish(),
140
140
  acl: AccessControl.array().optional(),
141
141
  });
142
142
  export function getCalPermissionsInfo(cal, user) {
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ addRoute({
34
34
  await checkAuthForUser(request, userId);
35
35
  return withEncoded(await database
36
36
  .insertInto('calendars')
37
- .values({ ...init, userId })
37
+ .values({ ...withDecoded(init), userId })
38
38
  .returningAll()
39
39
  .executeTakeFirstOrThrow()
40
40
  .catch(withError('Could not create calendar')));
@@ -52,7 +52,7 @@ addRoute({
52
52
  await authRequestForItem(request, 'calendars', id, { edit: true });
53
53
  return withEncoded(await database
54
54
  .updateTable('calendars')
55
- .set(body)
55
+ .set(withDecoded(body))
56
56
  .where('id', '=', id)
57
57
  .returningAll()
58
58
  .executeTakeFirstOrThrow()
@@ -0,0 +1,8 @@
1
+ import en from '../locales/en.json';
2
+ import './common.js';
3
+ type en = typeof en;
4
+ declare module '@axium/client/locales' {
5
+ interface Locale extends en {
6
+ }
7
+ }
8
+ export {};
@@ -0,0 +1,4 @@
1
+ import { extendLocale } from '@axium/client';
2
+ import en from '../locales/en.json' with { type: 'json' };
3
+ import './common.js';
4
+ extendLocale('en', en);
package/lib/Event.svelte CHANGED
@@ -2,6 +2,7 @@
2
2
  import type { EventInitProp } from '@axium/calendar/client';
3
3
  import type { Event } from '@axium/calendar/common';
4
4
  import { eventToICS, formatEventTimes, toRRuleDate } from '@axium/calendar/common';
5
+ import { text } from '@axium/client';
5
6
  import { contextMenu } from '@axium/client/attachments';
6
7
  import { Icon, Popover } from '@axium/client/components';
7
8
  import { colorHashHex, decodeColor, encodeColor } from '@axium/core/color';
@@ -43,8 +44,8 @@
43
44
  {@attach contextMenu(
44
45
  {
45
46
  i: 'pencil',
46
- text: 'Edit',
47
- action: () => {
47
+ text: text('Event.edit'),
48
+ action() {
48
49
  eventEditId = event.id;
49
50
  eventEditCalId = event.calId;
50
51
  eventInit = initData;
@@ -53,13 +54,13 @@
53
54
  },
54
55
  {
55
56
  i: 'file-export',
56
- text: 'Export .ics',
57
+ text: text('Event.export'),
57
58
  action: () => download(event.summary + '.ics', eventToICS(event)),
58
59
  },
59
60
  {
60
61
  i: 'trash-can',
61
- text: 'Delete',
62
- action: () => {
62
+ text: text('Event.delete'),
63
+ action() {
63
64
  eventEditId = event.id;
64
65
  document.querySelector<HTMLDialogElement>('#event-delete')!.showModal();
65
66
  },
@@ -0,0 +1,40 @@
1
+ {
2
+ "app_name": {
3
+ "calendar": "Calendar"
4
+ },
5
+ "Event": {
6
+ "edit": "Edit",
7
+ "export": "Export .ics",
8
+ "delete": "Delete"
9
+ },
10
+ "calendar": {
11
+ "new_event": "New Event",
12
+ "today": "Today",
13
+ "list_owned": "My Calendars",
14
+ "list_shared": "Shared Calendars",
15
+ "delete_confirm": "Are you sure you want to delete the calendar \"{name}\"?",
16
+ "edit": "Edit"
17
+ },
18
+ "calendar_init": {
19
+ "name": "Name",
20
+ "color": "Color",
21
+ "submit_edit": "Save"
22
+ },
23
+ "event_delete": {
24
+ "confirm": "Are you sure you want to delete this event?"
25
+ },
26
+ "event_init": {
27
+ "all_day": "All day",
28
+ "description_placeholder": "Add description",
29
+ "location_placeholder": "Add location",
30
+ "recurrence": {
31
+ "daily": "Every day",
32
+ "monthly_on": "Every month on the {day}",
33
+ "none": "Does not repeat",
34
+ "weekly": "Every week on {day}",
35
+ "yearly": "Every year on {date}"
36
+ },
37
+ "submit_edit": "Update",
38
+ "title_placeholder": "Add title"
39
+ }
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/calendar",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "Calendar for Axium",
6
6
  "funding": {
@@ -29,6 +29,7 @@
29
29
  "dist",
30
30
  "lib",
31
31
  "routes",
32
+ "locales",
32
33
  "db.json"
33
34
  ],
34
35
  "scripts": {
@@ -43,13 +44,15 @@
43
44
  },
44
45
  "dependencies": {
45
46
  "rrule": "^2.8.1",
47
+ "svelte-gestures": "^5.2.2",
46
48
  "zod": "^4.0.5"
47
49
  },
48
50
  "axium": {
49
51
  "server": {
50
52
  "routes": "routes",
51
53
  "hooks": "./dist/hooks.js",
52
- "db": "./db.json"
54
+ "db": "./db.json",
55
+ "web_client_hooks": "./dist/web_hook.js"
53
56
  },
54
57
  "apps": [
55
58
  {
@@ -2,6 +2,7 @@
2
2
  import { getEvents, type EventInitFormData, type EventInitProp } from '@axium/calendar/client';
3
3
  import type { Event } from '@axium/calendar/common';
4
4
  import {
5
+ CalendarInit,
5
6
  dateToInputValue,
6
7
  fromRRuleDate,
7
8
  getCalPermissionsInfo,
@@ -13,11 +14,12 @@
13
14
  withOrdinalSuffix,
14
15
  } from '@axium/calendar/common';
15
16
  import * as Cal from '@axium/calendar/components';
17
+ import { fetchAPI, text } from '@axium/client';
16
18
  import { contextMenu, dynamicRows } from '@axium/client/attachments';
17
19
  import { AccessControlDialog, ColorPicker, FormDialog, Icon, Popover, UserDiscovery } from '@axium/client/components';
18
- import { fetchAPI } from '@axium/client/requests';
19
20
  import { colorHashHex, encodeColor } from '@axium/core/color';
20
21
  import { rrulestr } from 'rrule';
22
+ import { useSwipe } from 'svelte-gestures';
21
23
  import { SvelteDate } from 'svelte/reactivity';
22
24
  import { _throw } from 'utilium';
23
25
  import * as z from 'zod';
@@ -69,6 +71,13 @@
69
71
  eventEditId = $state<string>(),
70
72
  eventEditCalId = $state<string>();
71
73
 
74
+ const defaultCalInit = {
75
+ color: null,
76
+ } as CalendarInit;
77
+
78
+ let calInit = $state<CalendarInit>(defaultCalInit),
79
+ calEditId = $state<string>();
80
+
72
81
  const recurringEvents = $derived(
73
82
  events
74
83
  .filter(ev => ev.recurrence)
@@ -81,20 +90,26 @@
81
90
  })
82
91
  );
83
92
 
84
- const defaultEventColor = $derived((eventInit.calendar || calendars[0])?.color || encodeColor(colorHashHex(user.name)));
93
+ const defaultCalColor = encodeColor(colorHashHex(user.name));
94
+ const defaultEventColor = $derived((eventInit.calendar || calendars[0])?.color || defaultCalColor);
85
95
 
86
96
  let calSidebar = $state<HTMLDivElement>();
87
97
  </script>
88
98
 
89
99
  <svelte:head>
90
- <title>Calendar</title>
100
+ <title>{text('app_name.calendar')}</title>
91
101
  </svelte:head>
92
102
 
93
103
  <div id="cal-app">
94
- <button class="event-init icon-text mobile-hide" command="show-modal" commandfor="event-init"><Icon i="plus" /> New Event</button>
104
+ <div id="event-init-container">
105
+ <button class="event-init icon-text mobile-hide" command="show-modal" commandfor="event-init">
106
+ <Icon i="plus" />
107
+ <span>{text('calendar.new_event')}</span>
108
+ </button>
109
+ </div>
95
110
  <div class="bar">
96
111
  <!-- desktop -->
97
- <button class="mobile-hide" onclick={() => start.setTime(today.getTime())}>Today</button>
112
+ <button class="mobile-hide" onclick={() => start.setTime(today.getTime())}>{text('calendar.today')}</button>
98
113
  <button
99
114
  class="reset mobile-hide"
100
115
  onclick={() => {
@@ -125,60 +140,66 @@
125
140
  <div id="cal-sidebar" bind:this={calSidebar}>
126
141
  <Cal.Select bind:start bind:end />
127
142
  <div class="cal-sidebar-header">
128
- <h4>My Calendars</h4>
129
- <button style:display="contents" command="show-modal" commandfor="add-calendar">
143
+ <h4>{text('calendar.list_owned')}</h4>
144
+ <button style:display="contents" command="show-modal" commandfor="cal-init">
130
145
  <Icon i="plus" />
131
146
  </button>
132
147
  </div>
133
148
  {#each calendars.filter(cal => cal.userId == user.id) as cal (cal.id)}
149
+ {@const edit = () => {
150
+ calEditId = cal.id;
151
+ calInit = cal;
152
+ document.querySelector<HTMLDialogElement>('#cal-init')!.showModal();
153
+ }}
134
154
  <div
135
155
  class="cal-sidebar-item"
136
156
  {@attach contextMenu(
137
- { i: 'pencil', text: 'Rename', action: () => dialogs['rename:' + cal.id].showModal() },
138
- { i: 'user-group', text: 'Share', action: () => dialogs['share:' + cal.id].showModal() },
139
- { i: 'trash', text: 'Delete', action: () => dialogs['delete:' + cal.id].showModal() }
157
+ { i: 'pencil', text: text('calendar.edit'), action: edit },
158
+ { i: 'user-group', text: text('generic.share'), action: () => dialogs['share:' + cal.id].showModal() },
159
+ { i: 'trash', text: text('generic.delete'), action: () => dialogs['delete:' + cal.id].showModal() }
140
160
  )}
141
161
  >
142
162
  <span>{cal.name}</span>
143
163
  <Popover showToggle="hover">
144
- <div class="menu-item" onclick={() => dialogs['rename:' + cal.id].showModal()}>
145
- <Icon i="pencil" /> Rename
146
- </div>
164
+ <button
165
+ class="reset menu-item"
166
+ command="show-modal"
167
+ commandfor="cal-init"
168
+ onclick={() => {
169
+ calEditId = cal.id;
170
+ calInit = { ...cal };
171
+ }}
172
+ >
173
+ <Icon i="pencil" />
174
+ <span>{text('calendar.edit')}</span>
175
+ </button>
147
176
  <div class="menu-item" onclick={() => dialogs['share:' + cal.id].showModal()}>
148
- <Icon i="user-group" /> Share
177
+ <Icon i="user-group" />
178
+ <span>{text('generic.share')}</span>
149
179
  </div>
150
180
  <div class="menu-item" onclick={() => dialogs['delete:' + cal.id].showModal()}>
151
- <Icon i="trash" /> Delete
181
+ <Icon i="trash" />
182
+ <span>{text('generic.delete')}</span>
152
183
  </div>
153
184
  </Popover>
154
- <FormDialog
155
- bind:dialog={dialogs['rename:' + cal.id]}
156
- submitText="Save"
157
- submit={(input: { name: string }) =>
158
- fetchAPI('PATCH', 'calendars/:id', input, cal.id).then(result => Object.assign(cal, result))}
159
- >
160
- <div>
161
- <label for="name">Name</label>
162
- <input name="name" type="text" required value={cal.name} />
163
- </div>
164
- </FormDialog>
165
185
  <AccessControlDialog editable itemType="calendars" item={cal} bind:dialog={dialogs['share:' + cal.id]} />
166
186
  <FormDialog
167
187
  bind:dialog={dialogs['delete:' + cal.id]}
168
- submitText="Delete"
188
+ submitText={text('generic.delete')}
169
189
  submitDanger
170
190
  submit={() => fetchAPI('DELETE', 'calendars/:id', null, cal.id).then(() => calendars.splice(calendars.indexOf(cal), 1))}
171
191
  >
172
192
  <p>
173
- Are you sure you want to delete the calendar "{cal.name}"?<br />
174
- <strong>This action cannot be undone.</strong>
193
+ <span>{text('calendar.delete_confirm', { name: cal.name })}</span>
194
+ <br />
195
+ <strong>{text('generic.action_irreversible')}</strong>
175
196
  </p>
176
197
  </FormDialog>
177
198
  </div>
178
199
  {/each}
179
200
  {#if calendars.some(cal => cal.userId != user.id)}
180
201
  <div class="cal-sidebar-header">
181
- <h4>Shared Calendars</h4>
202
+ <h4>{text('calendar.list_shared')}</h4>
182
203
  </div>
183
204
  {#each calendars.filter(cal => cal.userId != user.id) as cal (cal.id)}
184
205
  {@const { list, icon } = getCalPermissionsInfo(cal, user)}
@@ -188,7 +209,27 @@
188
209
  {/each}
189
210
  {/if}
190
211
  </div>
191
- <div id="cal">
212
+ <div
213
+ id="cal"
214
+ {...useSwipe(
215
+ e => {
216
+ if (e.detail.pointerType != 'touch') return;
217
+ switch (e.detail.direction) {
218
+ case 'left':
219
+ start.setDate(start.getDate() + spanDays);
220
+ end.setDate(end.getDate() + spanDays);
221
+ break;
222
+ case 'right':
223
+ start.setDate(start.getDate() - spanDays);
224
+ end.setDate(end.getDate() - spanDays);
225
+ break;
226
+ case 'top':
227
+ case 'bottom':
228
+ }
229
+ },
230
+ () => ({ touchAction: 'pan-y' })
231
+ )}
232
+ >
192
233
  <div id="hours" class="subtle">
193
234
  {#each { length: 24 }, i}
194
235
  {#if !i}
@@ -241,8 +282,12 @@
241
282
  <FormDialog
242
283
  id="event-init"
243
284
  clearOnCancel
244
- cancel={() => (eventInit = defaultEventInit)}
245
- submitText={eventEditId ? 'Update' : 'Create'}
285
+ cancel={() => {
286
+ eventInit = defaultEventInit;
287
+ eventEditId = undefined;
288
+ eventEditCalId = undefined;
289
+ }}
290
+ submitText={text(eventEditId ? 'event_init.submit_edit' : 'generic.create')}
246
291
  submit={async (data: EventInitFormData) => {
247
292
  Object.assign(eventInit, data);
248
293
  const calendar = calendars.find(cal => cal.id == eventInit.calId);
@@ -262,7 +307,7 @@
262
307
  return;
263
308
  }}
264
309
  >
265
- <input name="summary" type="text" required placeholder="Add title" bind:value={eventInit.summary} />
310
+ <input name="summary" type="text" required placeholder={text('event_init.title_placeholder')} bind:value={eventInit.summary} />
266
311
  <div class="event-times-container">
267
312
  <label for="eventInit.start"><Icon i="clock" /></label>
268
313
  <div class="event-times">
@@ -287,22 +332,24 @@
287
332
  <label for="eventInit.isAllDay:checkbox" class="checkbox">
288
333
  {#if eventInit.isAllDay}<Icon i="check" --size="1.3em" />{/if}
289
334
  </label>
290
- <label for="eventInit.isAllDay:checkbox">All day</label>
335
+ <label for="eventInit.isAllDay:checkbox">{text('event_init.all_day')}</label>
291
336
  <div class="spacing"></div>
292
337
  <select name="recurrence" bind:value={eventInit.recurrence}>
293
- <option value="">Does not repeat</option>
294
- <option value="FREQ=DAILY">Every day</option>
338
+ <option value="">{text('event_init.recurrence.none')}</option>
339
+ <option value="FREQ=DAILY">{text('event_init.recurrence.daily')}</option>
295
340
  <option value="FREQ=WEEKLY;BYDAY={toByDay(eventInit.start)}">
296
- Every week on {longWeekDay(eventInit.start)}
341
+ {text('event_init.recurrence.weekly', { day: longWeekDay(eventInit.start) })}
297
342
  </option>
298
343
  <option value="FREQ=MONTHLY;BYDAY={Math.ceil(eventInit.start.getDate() / 7) + toByDay(eventInit.start)}"
299
- >Every month on the {weekDayOfMonth(eventInit.start)}
344
+ >{text('event_init.recurrence.monthly_on', { day: weekDayOfMonth(eventInit.start) })}
300
345
  </option>
301
346
  <option value="FREQ=MONTHLY;BYMONTHDAY={eventInit.start.getDate()}">
302
- Every month on the {withOrdinalSuffix(eventInit.start.getDate())}
347
+ {text('event_init.recurrence.monthly_on', { day: withOrdinalSuffix(eventInit.start.getDate()) })}
303
348
  </option>
304
349
  <option value="FREQ=YEARLY;BYMONTH={eventInit.start.getMonth()};BYMONTHDAY={eventInit.start.getDate()}">
305
- Every year on {eventInit.start.toLocaleDateString('default', { month: 'long', day: 'numeric' })}
350
+ {text('event_init.recurrence.yearly', {
351
+ date: eventInit.start.toLocaleDateString('default', { month: 'long', day: 'numeric' }),
352
+ })}
306
353
  </option>
307
354
  <!-- @todo <option value="">Custom</option> -->
308
355
  </select>
@@ -343,7 +390,12 @@
343
390
 
344
391
  <div>
345
392
  <label for="eventInit.location"><Icon i="location-dot" /></label>
346
- <input name="location" id="eventInit.location" placeholder="Add location" bind:value={eventInit.location} />
393
+ <input
394
+ name="location"
395
+ id="eventInit.location"
396
+ placeholder={text('event_init.location_placeholder')}
397
+ bind:value={eventInit.location}
398
+ />
347
399
  </div>
348
400
 
349
401
  <div>
@@ -351,7 +403,7 @@
351
403
  <textarea
352
404
  name="description"
353
405
  id="eventInit.description"
354
- placeholder="Add description"
406
+ placeholder={text('event_init.description_placeholder')}
355
407
  bind:value={eventInit.description}
356
408
  {@attach dynamicRows()}
357
409
  ></textarea>
@@ -360,7 +412,7 @@
360
412
 
361
413
  <FormDialog
362
414
  id="event-delete"
363
- submitText="Delete"
415
+ submitText={text('generic.delete')}
364
416
  submitDanger
365
417
  submit={async () => {
366
418
  if (!eventEditId) throw new Error('No event to delete');
@@ -371,17 +423,41 @@
371
423
  eventEditId = undefined;
372
424
  }}
373
425
  >
374
- <p>Are you sure you want to delete this event?</p>
426
+ <p>{text('event_delete.confirm')}</p>
375
427
  </FormDialog>
376
428
 
377
429
  <FormDialog
378
- id="add-calendar"
379
- submitText="Create"
380
- submit={(input: { name: string }) =>
381
- fetchAPI('PUT', 'users/:id/calendars', input, user.id).then(cal => calendars.push({ ...cal, acl: [] }))}
430
+ id="cal-init"
431
+ clearOnCancel
432
+ cancel={() => {
433
+ calInit = defaultCalInit;
434
+ calEditId = undefined;
435
+ }}
436
+ submitText={text(calEditId ? 'calendar_init.submit_edit' : 'generic.create')}
437
+ submit={async (data: Record<keyof CalendarInit, string>) => {
438
+ Object.assign(calInit, data);
439
+
440
+ if (!calEditId) {
441
+ const cal = await fetchAPI('PUT', 'users/:id/calendars', calInit, user.id);
442
+ calendars.push({ ...cal, acl: [] });
443
+ return;
444
+ }
445
+
446
+ const cal = calendars.find(cal => cal.id == calEditId);
447
+
448
+ const result = await fetchAPI('PATCH', 'calendars/:id', calInit, calEditId);
449
+
450
+ if (cal) Object.assign(cal, result);
451
+ else console.warn('Could not find calendar to update');
452
+ }}
382
453
  >
383
454
  <div>
384
- <label for="name">Name</label>
385
- <input name="name" type="text" required />
455
+ <label for="calInit.name">{text('calendar_init.name')}</label>
456
+ <input id="calInit.name" bind:value={calInit.name} required />
457
+ </div>
458
+
459
+ <div>
460
+ <label for="calInit.color">{text('calendar_init.color')}</label>
461
+ <ColorPicker bind:value={calInit.color} defaultValue={defaultCalColor} />
386
462
  </div>
387
463
  </FormDialog>
@@ -15,11 +15,18 @@ body:has(#cal-app) {
15
15
  }
16
16
  }
17
17
 
18
+ #event-init-container {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ padding: 0 1em;
23
+ }
24
+
18
25
  button.event-init {
19
- margin: 0.5em;
20
26
  background-color: var(--bg-alt);
21
27
  text-align: center;
22
28
  justify-content: center;
29
+ width: 100%;
23
30
  }
24
31
 
25
32
  form {
@@ -81,6 +88,10 @@ div.attendees {
81
88
  align-items: center;
82
89
  font-weight: bold;
83
90
 
91
+ button {
92
+ font-weight: normal;
93
+ }
94
+
84
95
  @media (width < 700px) {
85
96
  margin: 0 1em;
86
97
 
@@ -224,7 +235,6 @@ div.attendees {
224
235
  inset: 3em 0 0;
225
236
  z-index: 10;
226
237
  background-color: var(--bg-menu);
227
- font-size: clamp(0.95rem, 0.75rem + 2.5cqw, 1.35rem);
228
238
  translate: -100% 0;
229
239
  opacity: 0;
230
240
  visibility: hidden;
@@ -234,6 +244,12 @@ div.attendees {
234
244
  width: 100%;
235
245
  }
236
246
 
247
+ .CalendarSelect,
248
+ .cal-sidebar-header,
249
+ .cal-sidebar-item > span {
250
+ font-size: clamp(0.95rem, 0.75rem + 2.5cqw, 1.35rem);
251
+ }
252
+
237
253
  &.active {
238
254
  translate: 0 0;
239
255
  opacity: 1;