@luckydye/calendar 1.1.1 → 1.1.2
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/package.json +4 -3
- package/src/ActiveCalendarStore.ts +96 -0
- package/src/CalDAVConfig.ts +1000 -0
- package/src/CalDAVSource.ts +506 -0
- package/src/CalendarIntegration.ts +68 -0
- package/src/CalendarInternal.ts +609 -0
- package/src/CalendarStorage.ts +54 -0
- package/src/CalendarView.ts +5290 -0
- package/src/Color.ts +64 -0
- package/src/GoogleCalendarSource.ts +717 -0
- package/src/ICal.ts +400 -0
- package/src/InMemorySource.ts +89 -0
- package/src/IndexedDBStorage.ts +393 -0
- package/src/InhouseBookingSource.ts +237 -0
- package/src/NotificationScheduler.ts +91 -0
- package/src/StatusBar.ts +128 -0
- package/src/StatusMessage.ts +122 -0
- package/src/Theme.ts +228 -0
- package/src/app.css +4 -0
- package/src/app.ts +932 -0
- package/src/lib.ts +4 -0
- package/src/service-worker.js +177 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import { css, html, LitElement } from "lit";
|
|
2
|
+
import type { CalendarSource } from "./CalendarIntegration.js";
|
|
3
|
+
import { authenticateWithGoogle } from "./GoogleCalendarSource.js";
|
|
4
|
+
import { InhouseBookingSource } from "./InhouseBookingSource.js";
|
|
5
|
+
import { CalDAVSource as CalDAVSourceClass } from "./CalDAVSource.js";
|
|
6
|
+
|
|
7
|
+
interface CalDAVSourceConfig extends CalendarSource {
|
|
8
|
+
type: "caldav";
|
|
9
|
+
credentials: {
|
|
10
|
+
serverUrl: string;
|
|
11
|
+
username: string;
|
|
12
|
+
password: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ICalSource extends CalendarSource {
|
|
17
|
+
type: "ical";
|
|
18
|
+
credentials: {
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GoogleSource extends CalendarSource {
|
|
24
|
+
type: "google";
|
|
25
|
+
credentials: {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
refreshToken?: string;
|
|
28
|
+
tokenExpiry?: string;
|
|
29
|
+
calendarId?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface InhouseSource extends CalendarSource {
|
|
34
|
+
type: "inhouse";
|
|
35
|
+
credentials: {
|
|
36
|
+
sessionCookie: string;
|
|
37
|
+
employeeId: string;
|
|
38
|
+
unitId?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ConfigurableSource = CalDAVSourceConfig | ICalSource | GoogleSource | InhouseSource;
|
|
43
|
+
|
|
44
|
+
export class CalDAVConfigElement extends LitElement {
|
|
45
|
+
static styles = css`
|
|
46
|
+
:host {
|
|
47
|
+
display: block;
|
|
48
|
+
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
|
49
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.container {
|
|
53
|
+
background: var(--bg-elevated, rgba(30, 30, 30, 0.95));
|
|
54
|
+
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
55
|
+
border-radius: var(--border-radius-lg, 8px);
|
|
56
|
+
padding: 16px;
|
|
57
|
+
box-sizing: border-box;
|
|
58
|
+
min-width: 400px;
|
|
59
|
+
max-width: 500px;
|
|
60
|
+
height: 100%;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.header {
|
|
64
|
+
display: flex;
|
|
65
|
+
justify-content: space-between;
|
|
66
|
+
align-items: center;
|
|
67
|
+
margin-bottom: 16px;
|
|
68
|
+
padding-bottom: 12px;
|
|
69
|
+
border-bottom: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.title {
|
|
73
|
+
font-size: 16px;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.close-btn {
|
|
79
|
+
background: none;
|
|
80
|
+
border: none;
|
|
81
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
82
|
+
font-size: 20px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
padding: 4px;
|
|
85
|
+
line-height: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.close-btn:hover {
|
|
89
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.sources-list {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 12px;
|
|
96
|
+
margin-bottom: 16px;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
max-height: calc(100% - 120px);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.source-item {
|
|
102
|
+
flex: none;
|
|
103
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.05));
|
|
104
|
+
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
105
|
+
border-radius: var(--border-radius, 6px);
|
|
106
|
+
padding: 12px;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.source-header {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
margin-bottom: 8px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.source-color {
|
|
118
|
+
width: 12px;
|
|
119
|
+
height: 12px;
|
|
120
|
+
border-radius: var(--border-radius-sm, 2px);
|
|
121
|
+
flex-shrink: 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.source-name {
|
|
125
|
+
flex: 1;
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
128
|
+
font-size: 14px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.source-enabled {
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.source-actions {
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.icon-btn {
|
|
141
|
+
background: none;
|
|
142
|
+
border: none;
|
|
143
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
padding: 4px;
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
line-height: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.icon-btn:hover {
|
|
151
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.source-url {
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.empty-state {
|
|
160
|
+
text-align: center;
|
|
161
|
+
padding: 24px;
|
|
162
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.4));
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.add-btn {
|
|
167
|
+
width: 100%;
|
|
168
|
+
padding: 10px;
|
|
169
|
+
background: var(--bg-button-hover, rgba(255, 255, 255, 0.1));
|
|
170
|
+
border: 1px dashed var(--grid-color-hover, rgba(255, 255, 255, 0.3));
|
|
171
|
+
border-radius: var(--border-radius, 6px);
|
|
172
|
+
color: var(--text-secondary, rgba(255, 255, 255, 0.7));
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
transition: all 0.15s;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.add-btn:hover {
|
|
179
|
+
background: var(--bg-item-hover, rgba(255, 255, 255, 0.15));
|
|
180
|
+
border-color: var(--grid-color-strong, rgba(255, 255, 255, 0.5));
|
|
181
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.form {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
gap: 12px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.form-group {
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
gap: 4px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.form-label {
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
199
|
+
text-transform: uppercase;
|
|
200
|
+
letter-spacing: 0.5px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.form-input {
|
|
204
|
+
background: var(--bg-input, rgba(0, 0, 0, 0.3));
|
|
205
|
+
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
206
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
207
|
+
padding: 8px 12px;
|
|
208
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
209
|
+
font-size: 14px;
|
|
210
|
+
outline: none;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.form-input:focus {
|
|
214
|
+
border-color: var(--grid-color-strong, rgba(255, 255, 255, 0.3));
|
|
215
|
+
background: var(--bg-input-focus, rgba(0, 0, 0, 0.5));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.color-picker {
|
|
219
|
+
display: flex;
|
|
220
|
+
gap: 8px;
|
|
221
|
+
flex-wrap: wrap;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.color-option {
|
|
225
|
+
width: 28px;
|
|
226
|
+
height: 28px;
|
|
227
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
border: 2px solid transparent;
|
|
230
|
+
transition: transform 0.15s;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.color-option:hover {
|
|
234
|
+
transform: scale(1.1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.color-option.selected {
|
|
238
|
+
border-color: var(--text-primary, rgba(255, 255, 255, 0.8));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.form-actions {
|
|
242
|
+
display: flex;
|
|
243
|
+
gap: 8px;
|
|
244
|
+
justify-content: flex-end;
|
|
245
|
+
margin-top: 8px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.btn {
|
|
249
|
+
padding: 8px 16px;
|
|
250
|
+
border-radius: var(--border-radius-sm, 4px);
|
|
251
|
+
font-size: 13px;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
transition: all 0.15s;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.btn-primary {
|
|
257
|
+
background: var(--accent-primary, rgba(100, 150, 255, 0.8));
|
|
258
|
+
border: none;
|
|
259
|
+
color: var(--text-inverse, white);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.btn-primary:hover {
|
|
263
|
+
background: var(--accent-primary, rgba(100, 150, 255, 1));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.btn-secondary {
|
|
267
|
+
background: transparent;
|
|
268
|
+
border: 1px solid var(--grid-color-hover, rgba(255, 255, 255, 0.2));
|
|
269
|
+
color: var(--text-secondary, rgba(255, 255, 255, 0.7));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.btn-secondary:hover {
|
|
273
|
+
border-color: var(--grid-color-strong, rgba(255, 255, 255, 0.4));
|
|
274
|
+
color: var(--text-primary, rgba(255, 255, 255, 0.9));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.sync-status {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 8px;
|
|
281
|
+
margin-top: 12px;
|
|
282
|
+
padding-top: 12px;
|
|
283
|
+
border-top: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.sync-spinner {
|
|
289
|
+
width: 14px;
|
|
290
|
+
height: 14px;
|
|
291
|
+
border: 2px solid var(--grid-color, rgba(255, 255, 255, 0.2));
|
|
292
|
+
border-top-color: var(--text-primary, rgba(255, 255, 255, 0.8));
|
|
293
|
+
border-radius: 50%;
|
|
294
|
+
animation: spin 1s linear infinite;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes spin {
|
|
298
|
+
to { transform: rotate(360deg); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.sync-success {
|
|
302
|
+
color: var(--accent-success, #7cb342);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.sync-error {
|
|
306
|
+
color: var(--accent-error, #e53935);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.google-auth-section {
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 12px;
|
|
313
|
+
padding: 16px;
|
|
314
|
+
background: var(--bg-item, rgba(255, 255, 255, 0.05));
|
|
315
|
+
border: 1px solid var(--grid-color, rgba(255, 255, 255, 0.1));
|
|
316
|
+
border-radius: var(--border-radius, 6px);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.google-login-btn {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
gap: 10px;
|
|
324
|
+
padding: 12px 16px;
|
|
325
|
+
background: white;
|
|
326
|
+
border: 1px solid #dadce0;
|
|
327
|
+
border-radius: 4px;
|
|
328
|
+
color: #3c4043;
|
|
329
|
+
font-family: "Google Sans", Roboto, Arial, sans-serif;
|
|
330
|
+
font-size: 14px;
|
|
331
|
+
font-weight: 500;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
transition: box-shadow 0.15s;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.google-login-btn:hover {
|
|
337
|
+
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.google-login-btn:disabled {
|
|
341
|
+
opacity: 0.6;
|
|
342
|
+
cursor: not-allowed;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.google-logo {
|
|
346
|
+
width: 18px;
|
|
347
|
+
height: 18px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.auth-status {
|
|
351
|
+
font-size: 13px;
|
|
352
|
+
color: var(--text-muted, rgba(255, 255, 255, 0.5));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.auth-status.authenticated {
|
|
356
|
+
color: var(--accent-success, #7cb342);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.auth-status.error {
|
|
360
|
+
color: var(--accent-error, #e53935);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.client-id-input {
|
|
364
|
+
font-size: 12px;
|
|
365
|
+
}
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
sources: ConfigurableSource[] = [];
|
|
369
|
+
isAdding = false;
|
|
370
|
+
editingId: string | null = null;
|
|
371
|
+
isGoogleAuthenticating = false;
|
|
372
|
+
googleAuthError: string | null = null;
|
|
373
|
+
|
|
374
|
+
private formData: Partial<ConfigurableSource> = {};
|
|
375
|
+
|
|
376
|
+
static get properties() {
|
|
377
|
+
return {
|
|
378
|
+
sources: { type: Array },
|
|
379
|
+
isAdding: { type: Boolean },
|
|
380
|
+
editingId: { type: String },
|
|
381
|
+
isGoogleAuthenticating: { type: Boolean },
|
|
382
|
+
googleAuthError: { type: String },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
constructor() {
|
|
387
|
+
super();
|
|
388
|
+
this.loadSources();
|
|
389
|
+
|
|
390
|
+
// Check for OAuth callback (authorization code flow)
|
|
391
|
+
if (typeof window !== 'undefined' && window.location.search) {
|
|
392
|
+
const params = new URLSearchParams(window.location.search);
|
|
393
|
+
const code = params.get('code');
|
|
394
|
+
const error = params.get('error');
|
|
395
|
+
const state = params.get('state');
|
|
396
|
+
|
|
397
|
+
if (code || error) {
|
|
398
|
+
// Send the result to the opener window
|
|
399
|
+
if (window.opener) {
|
|
400
|
+
window.opener.postMessage({
|
|
401
|
+
type: 'google-oauth-callback',
|
|
402
|
+
code: code,
|
|
403
|
+
error: error,
|
|
404
|
+
receivedState: state,
|
|
405
|
+
}, window.location.origin);
|
|
406
|
+
window.close();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
connectedCallback() {
|
|
413
|
+
super.connectedCallback();
|
|
414
|
+
this.loadSources();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
loadSources() {
|
|
418
|
+
const saved = localStorage.getItem("caldav-sources");
|
|
419
|
+
if (saved) {
|
|
420
|
+
try {
|
|
421
|
+
this.sources = JSON.parse(saved);
|
|
422
|
+
} catch {
|
|
423
|
+
this.sources = [];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
saveSources() {
|
|
430
|
+
localStorage.setItem("caldav-sources", JSON.stringify(this.sources));
|
|
431
|
+
this.dispatchEvent(new CustomEvent("sources-changed", {
|
|
432
|
+
detail: { sources: this.sources },
|
|
433
|
+
bubbles: true,
|
|
434
|
+
}));
|
|
435
|
+
this.requestUpdate();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
addSource() {
|
|
439
|
+
this.isAdding = true;
|
|
440
|
+
this.editingId = null;
|
|
441
|
+
this.formData = {
|
|
442
|
+
type: "caldav",
|
|
443
|
+
color: "#FF6E68",
|
|
444
|
+
enabled: true,
|
|
445
|
+
};
|
|
446
|
+
this.requestUpdate();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
editSource(source: ConfigurableSource) {
|
|
450
|
+
this.isAdding = false;
|
|
451
|
+
this.editingId = source.id;
|
|
452
|
+
this.formData = { ...source };
|
|
453
|
+
this.requestUpdate();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
deleteSource(id: string) {
|
|
457
|
+
this.sources = this.sources.filter((s) => s.id !== id);
|
|
458
|
+
this.saveSources();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
toggleEnabled(source: ConfigurableSource) {
|
|
462
|
+
source.enabled = !source.enabled;
|
|
463
|
+
this.saveSources();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
saveForm() {
|
|
467
|
+
if (!this.formData.name?.trim()) {
|
|
468
|
+
alert('Please enter a calendar name');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!this.formData.type) {
|
|
473
|
+
alert('Please select a calendar type');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
let source: ConfigurableSource;
|
|
478
|
+
|
|
479
|
+
if (this.formData.type === "caldav") {
|
|
480
|
+
if (
|
|
481
|
+
!this.formData.credentials?.serverUrl ||
|
|
482
|
+
!this.formData.credentials?.username ||
|
|
483
|
+
!this.formData.credentials?.password
|
|
484
|
+
) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
source = {
|
|
489
|
+
id: this.editingId || crypto.randomUUID(),
|
|
490
|
+
name: this.formData.name,
|
|
491
|
+
type: "caldav",
|
|
492
|
+
credentials: {
|
|
493
|
+
serverUrl: this.formData.credentials.serverUrl,
|
|
494
|
+
username: this.formData.credentials.username,
|
|
495
|
+
password: this.formData.credentials.password,
|
|
496
|
+
},
|
|
497
|
+
color: this.formData.color || "#FF6E68",
|
|
498
|
+
enabled: this.formData.enabled ?? true,
|
|
499
|
+
} as CalDAVSourceConfig;
|
|
500
|
+
} else if (this.formData.type === "ical") {
|
|
501
|
+
if (!this.formData.credentials?.url) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
source = {
|
|
506
|
+
id: this.editingId || crypto.randomUUID(),
|
|
507
|
+
name: this.formData.name,
|
|
508
|
+
type: "ical",
|
|
509
|
+
credentials: {
|
|
510
|
+
url: this.formData.credentials.url,
|
|
511
|
+
},
|
|
512
|
+
color: this.formData.color || "#FF6E68",
|
|
513
|
+
enabled: this.formData.enabled ?? true,
|
|
514
|
+
} as ICalSource;
|
|
515
|
+
} else if (this.formData.type === "google") {
|
|
516
|
+
if (!this.formData.credentials?.accessToken) {
|
|
517
|
+
alert('Please authenticate with Google before adding the calendar');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
source = {
|
|
522
|
+
id: this.editingId || crypto.randomUUID(),
|
|
523
|
+
name: this.formData.name,
|
|
524
|
+
type: "google",
|
|
525
|
+
credentials: {
|
|
526
|
+
accessToken: this.formData.credentials.accessToken,
|
|
527
|
+
refreshToken: this.formData.credentials.refreshToken,
|
|
528
|
+
tokenExpiry: this.formData.credentials.tokenExpiry,
|
|
529
|
+
calendarId: this.formData.credentials.calendarId || "primary",
|
|
530
|
+
},
|
|
531
|
+
color: this.formData.color || "#4285F4",
|
|
532
|
+
enabled: this.formData.enabled ?? true,
|
|
533
|
+
} as GoogleSource;
|
|
534
|
+
} else if (this.formData.type === "inhouse") {
|
|
535
|
+
if (!this.formData.credentials?.sessionCookie || !this.formData.credentials?.employeeId) {
|
|
536
|
+
alert('Please enter session cookie and employee ID');
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
source = {
|
|
541
|
+
id: this.editingId || crypto.randomUUID(),
|
|
542
|
+
name: this.formData.name,
|
|
543
|
+
type: "inhouse",
|
|
544
|
+
credentials: {
|
|
545
|
+
sessionCookie: this.formData.credentials.sessionCookie,
|
|
546
|
+
employeeId: this.formData.credentials.employeeId,
|
|
547
|
+
unitId: this.formData.credentials.unitId,
|
|
548
|
+
},
|
|
549
|
+
color: this.formData.color || "#FF6E68",
|
|
550
|
+
enabled: this.formData.enabled ?? true,
|
|
551
|
+
} as InhouseSource;
|
|
552
|
+
} else {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (this.editingId) {
|
|
557
|
+
const index = this.sources.findIndex((s) => s.id === this.editingId);
|
|
558
|
+
if (index >= 0) {
|
|
559
|
+
this.sources[index] = source;
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
this.sources.push(source);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
this.isAdding = false;
|
|
566
|
+
this.editingId = null;
|
|
567
|
+
this.formData = {};
|
|
568
|
+
this.googleAuthError = null;
|
|
569
|
+
this.saveSources();
|
|
570
|
+
this.requestUpdate();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
cancelForm() {
|
|
574
|
+
this.isAdding = false;
|
|
575
|
+
this.editingId = null;
|
|
576
|
+
this.formData = {};
|
|
577
|
+
this.googleAuthError = null;
|
|
578
|
+
this.isGoogleAuthenticating = false;
|
|
579
|
+
this.requestUpdate();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
updateForm(field: string, value: string | boolean) {
|
|
583
|
+
if (field === "serverUrl" || field === "username" || field === "password" || field === "url" ||
|
|
584
|
+
field === "accessToken" || field === "refreshToken" || field === "tokenExpiry" || field === "calendarId" ||
|
|
585
|
+
field === "sessionCookie" || field === "employeeId" || field === "unitId") {
|
|
586
|
+
this.formData = {
|
|
587
|
+
...this.formData,
|
|
588
|
+
credentials: {
|
|
589
|
+
...this.formData.credentials,
|
|
590
|
+
[field]: value,
|
|
591
|
+
} as ConfigurableSource["credentials"],
|
|
592
|
+
};
|
|
593
|
+
} else {
|
|
594
|
+
this.formData = { ...this.formData, [field]: value };
|
|
595
|
+
}
|
|
596
|
+
this.requestUpdate();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async signInWithGoogle() {
|
|
600
|
+
this.isGoogleAuthenticating = true;
|
|
601
|
+
this.googleAuthError = null;
|
|
602
|
+
this.requestUpdate();
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
// Load Google OAuth credentials from public directory
|
|
606
|
+
const credentialsResponse = await fetch('/credentials_google.json');
|
|
607
|
+
if (!credentialsResponse.ok) {
|
|
608
|
+
throw new Error('Google OAuth credentials file not found. Please add credentials_google.json to the public directory.');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const credentials = await credentialsResponse.json();
|
|
612
|
+
const clientId = credentials.installed?.client_id || credentials.web?.client_id;
|
|
613
|
+
const clientSecret = credentials.installed?.client_secret || credentials.web?.client_secret;
|
|
614
|
+
|
|
615
|
+
if (!clientId || !clientSecret) {
|
|
616
|
+
throw new Error('Invalid credentials file format. Expected client_id and client_secret.');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const tokens = await authenticateWithGoogle(clientId, clientSecret);
|
|
620
|
+
|
|
621
|
+
// Fetch the list of calendars from Google
|
|
622
|
+
const calendarListResponse = await fetch(
|
|
623
|
+
'https://www.googleapis.com/calendar/v3/users/me/calendarList',
|
|
624
|
+
{
|
|
625
|
+
headers: {
|
|
626
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (!calendarListResponse.ok) {
|
|
632
|
+
throw new Error('Failed to fetch calendar list from Google');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const calendarList = await calendarListResponse.json();
|
|
636
|
+
|
|
637
|
+
// Create a source for each calendar
|
|
638
|
+
for (const calendar of calendarList.items || []) {
|
|
639
|
+
const newSource: GoogleSource = {
|
|
640
|
+
id: crypto.randomUUID(),
|
|
641
|
+
name: calendar.summary || 'Untitled Calendar',
|
|
642
|
+
type: 'google',
|
|
643
|
+
credentials: {
|
|
644
|
+
accessToken: tokens.accessToken,
|
|
645
|
+
refreshToken: tokens.refreshToken,
|
|
646
|
+
tokenExpiry: tokens.expiry,
|
|
647
|
+
calendarId: calendar.id,
|
|
648
|
+
},
|
|
649
|
+
color: calendar.backgroundColor || '#4285F4',
|
|
650
|
+
enabled: true,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
this.sources.push(newSource);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
this.saveSources();
|
|
657
|
+
this.isAdding = false;
|
|
658
|
+
this.formData = {};
|
|
659
|
+
this.isGoogleAuthenticating = false;
|
|
660
|
+
this.requestUpdate();
|
|
661
|
+
} catch (error) {
|
|
662
|
+
this.isGoogleAuthenticating = false;
|
|
663
|
+
this.googleAuthError = error instanceof Error ? error.message : "Authentication failed";
|
|
664
|
+
this.requestUpdate();
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
close() {
|
|
669
|
+
this.dispatchEvent(new CustomEvent("close", { bubbles: true }));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
render() {
|
|
673
|
+
const colors = [
|
|
674
|
+
"#FF6E68",
|
|
675
|
+
"#FFB800",
|
|
676
|
+
"#83D754",
|
|
677
|
+
"#0095FD",
|
|
678
|
+
"#9FC6E7",
|
|
679
|
+
"#888082",
|
|
680
|
+
"#E91E63",
|
|
681
|
+
"#9C27B0",
|
|
682
|
+
"#673AB7",
|
|
683
|
+
"#3F51B5",
|
|
684
|
+
"#2196F3",
|
|
685
|
+
"#00BCD4",
|
|
686
|
+
"#009688",
|
|
687
|
+
"#4CAF50",
|
|
688
|
+
"#8BC34A",
|
|
689
|
+
"#CDDC39",
|
|
690
|
+
"#FFEB3B",
|
|
691
|
+
"#FFC107",
|
|
692
|
+
"#FF9800",
|
|
693
|
+
"#FF5722",
|
|
694
|
+
"#795548",
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
return html`
|
|
698
|
+
<div class="container">
|
|
699
|
+
<div class="header">
|
|
700
|
+
<span class="title">Calendar Sources</span>
|
|
701
|
+
<button class="close-btn" @click=${this.close}>×</button>
|
|
702
|
+
</div>
|
|
703
|
+
|
|
704
|
+
${this.isAdding || this.editingId
|
|
705
|
+
? this.renderForm(colors)
|
|
706
|
+
: this.renderSourcesList()}
|
|
707
|
+
</div>
|
|
708
|
+
`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
renderSourcesList() {
|
|
712
|
+
if (this.sources.length === 0) {
|
|
713
|
+
return html`
|
|
714
|
+
<div class="empty-state">
|
|
715
|
+
No calendar sources configured
|
|
716
|
+
</div>
|
|
717
|
+
<button class="add-btn" @click=${this.addSource}>
|
|
718
|
+
+ Add Calendar Source
|
|
719
|
+
</button>
|
|
720
|
+
`;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
console.log(this.sources);
|
|
724
|
+
|
|
725
|
+
return html`
|
|
726
|
+
<div class="sources-list">
|
|
727
|
+
${this.sources.map(
|
|
728
|
+
(source) => {
|
|
729
|
+
let urlDisplay: string;
|
|
730
|
+
if (source.type === "caldav") {
|
|
731
|
+
urlDisplay = (source.credentials as CalDAVSourceConfig["credentials"]).serverUrl;
|
|
732
|
+
} else if (source.type === "ical") {
|
|
733
|
+
urlDisplay = (source.credentials as ICalSource["credentials"]).url;
|
|
734
|
+
} else if (source.type === "google") {
|
|
735
|
+
urlDisplay = (source.credentials as GoogleSource["credentials"]).calendarId || "primary";
|
|
736
|
+
} else if (source.type === "inhouse") {
|
|
737
|
+
urlDisplay = `Employee: ${(source.credentials as InhouseSource["credentials"]).employeeId}`;
|
|
738
|
+
} else {
|
|
739
|
+
urlDisplay = "Unknown source";
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return html`
|
|
743
|
+
<div class="source-item">
|
|
744
|
+
<div class="source-header">
|
|
745
|
+
<div
|
|
746
|
+
class="source-color"
|
|
747
|
+
style="background: ${source.color}"
|
|
748
|
+
></div>
|
|
749
|
+
<span class="source-name">${source.name}</span>
|
|
750
|
+
<input
|
|
751
|
+
type="checkbox"
|
|
752
|
+
class="source-enabled"
|
|
753
|
+
.checked=${source.enabled}
|
|
754
|
+
@change=${() => this.toggleEnabled(source)}
|
|
755
|
+
title="${source.enabled ? "Disable" : "Enable"} sync"
|
|
756
|
+
/>
|
|
757
|
+
<div class="source-actions">
|
|
758
|
+
<button
|
|
759
|
+
class="icon-btn"
|
|
760
|
+
@click=${() => this.editSource(source)}
|
|
761
|
+
title="Edit"
|
|
762
|
+
>
|
|
763
|
+
✎
|
|
764
|
+
</button>
|
|
765
|
+
<button
|
|
766
|
+
class="icon-btn"
|
|
767
|
+
@click=${() => this.deleteSource(source.id)}
|
|
768
|
+
title="Delete"
|
|
769
|
+
>
|
|
770
|
+
🗑
|
|
771
|
+
</button>
|
|
772
|
+
</div>
|
|
773
|
+
</div>
|
|
774
|
+
<div class="source-url">${source.type.toUpperCase()}: ${urlDisplay || "No URL"}</div>
|
|
775
|
+
</div>
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
)}
|
|
779
|
+
</div>
|
|
780
|
+
<button class="add-btn" @click=${this.addSource}>
|
|
781
|
+
+ Add Calendar Source
|
|
782
|
+
</button>
|
|
783
|
+
`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
renderForm(colors: string[]) {
|
|
787
|
+
const isEditing = this.editingId !== null;
|
|
788
|
+
const sourceType = this.formData.type || "caldav";
|
|
789
|
+
|
|
790
|
+
return html`
|
|
791
|
+
<div class="form">
|
|
792
|
+
<div class="form-group">
|
|
793
|
+
<label class="form-label">Type</label>
|
|
794
|
+
<select
|
|
795
|
+
class="form-input"
|
|
796
|
+
.value=${sourceType}
|
|
797
|
+
@change=${(e: Event) => {
|
|
798
|
+
const type = (e.target as HTMLSelectElement).value;
|
|
799
|
+
this.updateForm("type", type);
|
|
800
|
+
// Set default color based on type
|
|
801
|
+
if (type === "google" && !this.formData.color) {
|
|
802
|
+
this.updateForm("color", "#4285F4");
|
|
803
|
+
} else if (type === "caldav" && !this.formData.color) {
|
|
804
|
+
this.updateForm("color", "#FF6E68");
|
|
805
|
+
}
|
|
806
|
+
}}
|
|
807
|
+
?disabled=${isEditing}
|
|
808
|
+
>
|
|
809
|
+
<option value="caldav">CalDAV (with credentials)</option>
|
|
810
|
+
<option value="ical">iCal URL</option>
|
|
811
|
+
<option value="google">Google Calendar</option>
|
|
812
|
+
<option value="inhouse">Inhouse Booking System</option>
|
|
813
|
+
</select>
|
|
814
|
+
</div>
|
|
815
|
+
|
|
816
|
+
<div class="form-group">
|
|
817
|
+
<label class="form-label">Name</label>
|
|
818
|
+
<input
|
|
819
|
+
class="form-input"
|
|
820
|
+
type="text"
|
|
821
|
+
placeholder="My Calendar"
|
|
822
|
+
.value=${this.formData.name || ""}
|
|
823
|
+
@input=${(e: Event) =>
|
|
824
|
+
this.updateForm("name", (e.target as HTMLInputElement).value)}
|
|
825
|
+
/>
|
|
826
|
+
</div>
|
|
827
|
+
|
|
828
|
+
${sourceType === "caldav"
|
|
829
|
+
? html`
|
|
830
|
+
<div class="form-group">
|
|
831
|
+
<label class="form-label">Server URL</label>
|
|
832
|
+
<input
|
|
833
|
+
class="form-input"
|
|
834
|
+
type="text"
|
|
835
|
+
placeholder="https://mail.example.com/caldav/users/username/"
|
|
836
|
+
.value=${this.formData.credentials?.serverUrl || ""}
|
|
837
|
+
@input=${(e: Event) =>
|
|
838
|
+
this.updateForm("serverUrl", (e.target as HTMLInputElement).value)}
|
|
839
|
+
/>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
<div class="form-group">
|
|
843
|
+
<label class="form-label">Username</label>
|
|
844
|
+
<input
|
|
845
|
+
class="form-input"
|
|
846
|
+
type="text"
|
|
847
|
+
.value=${this.formData.credentials?.username || ""}
|
|
848
|
+
@input=${(e: Event) =>
|
|
849
|
+
this.updateForm("username", (e.target as HTMLInputElement).value)}
|
|
850
|
+
/>
|
|
851
|
+
</div>
|
|
852
|
+
|
|
853
|
+
<div class="form-group">
|
|
854
|
+
<label class="form-label">Password</label>
|
|
855
|
+
<input
|
|
856
|
+
class="form-input"
|
|
857
|
+
type="password"
|
|
858
|
+
.value=${this.formData.credentials?.password || ""}
|
|
859
|
+
@input=${(e: Event) =>
|
|
860
|
+
this.updateForm("password", (e.target as HTMLInputElement).value)}
|
|
861
|
+
/>
|
|
862
|
+
</div>
|
|
863
|
+
`
|
|
864
|
+
: sourceType === "ical"
|
|
865
|
+
? html`
|
|
866
|
+
<div class="form-group">
|
|
867
|
+
<label class="form-label">iCal URL</label>
|
|
868
|
+
<input
|
|
869
|
+
class="form-input"
|
|
870
|
+
type="text"
|
|
871
|
+
placeholder="https://example.com/calendar.ics"
|
|
872
|
+
.value=${this.formData.credentials?.url || ""}
|
|
873
|
+
@input=${(e: Event) =>
|
|
874
|
+
this.updateForm("url", (e.target as HTMLInputElement).value)}
|
|
875
|
+
/>
|
|
876
|
+
</div>
|
|
877
|
+
`
|
|
878
|
+
: sourceType === "google"
|
|
879
|
+
? html`
|
|
880
|
+
<div class="google-auth-section">
|
|
881
|
+
${this.formData.credentials?.accessToken
|
|
882
|
+
? html`
|
|
883
|
+
<div class="auth-status authenticated">
|
|
884
|
+
✓ Authenticated with Google
|
|
885
|
+
</div>
|
|
886
|
+
`
|
|
887
|
+
: html`
|
|
888
|
+
<button
|
|
889
|
+
class="google-login-btn"
|
|
890
|
+
@click=${this.signInWithGoogle}
|
|
891
|
+
?disabled=${this.isGoogleAuthenticating}
|
|
892
|
+
>
|
|
893
|
+
${this.isGoogleAuthenticating
|
|
894
|
+
? html`<span>Signing in...</span>`
|
|
895
|
+
: html`
|
|
896
|
+
<svg class="google-logo" viewBox="0 0 24 24">
|
|
897
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
898
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
899
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
900
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
901
|
+
</svg>
|
|
902
|
+
<span>Sign in with Google</span>
|
|
903
|
+
`}
|
|
904
|
+
</button>
|
|
905
|
+
`}
|
|
906
|
+
${this.googleAuthError
|
|
907
|
+
? html`<div class="auth-status error">${this.googleAuthError}</div>`
|
|
908
|
+
: null}
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<div class="form-group">
|
|
912
|
+
<label class="form-label">Calendar ID (optional)</label>
|
|
913
|
+
<input
|
|
914
|
+
class="form-input"
|
|
915
|
+
type="text"
|
|
916
|
+
placeholder="primary"
|
|
917
|
+
.value=${this.formData.credentials?.calendarId || ""}
|
|
918
|
+
@input=${(e: Event) =>
|
|
919
|
+
this.updateForm("calendarId", (e.target as HTMLInputElement).value)}
|
|
920
|
+
/>
|
|
921
|
+
</div>
|
|
922
|
+
`
|
|
923
|
+
: sourceType === "inhouse"
|
|
924
|
+
? html`
|
|
925
|
+
<div class="form-group">
|
|
926
|
+
<label class="form-label">Session Cookie</label>
|
|
927
|
+
<input
|
|
928
|
+
class="form-input"
|
|
929
|
+
type="password"
|
|
930
|
+
placeholder="sessionid=abc123; csrftoken=xyz789"
|
|
931
|
+
.value=${this.formData.credentials?.sessionCookie || ""}
|
|
932
|
+
@input=${(e: Event) =>
|
|
933
|
+
this.updateForm("sessionCookie", (e.target as HTMLInputElement).value)}
|
|
934
|
+
/>
|
|
935
|
+
<small style="color: var(--text-muted, rgba(255, 255, 255, 0.5)); font-size: 11px;">
|
|
936
|
+
Copy from browser DevTools after logging in
|
|
937
|
+
</small>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<div class="form-group">
|
|
941
|
+
<label class="form-label">Employee ID</label>
|
|
942
|
+
<input
|
|
943
|
+
class="form-input"
|
|
944
|
+
type="text"
|
|
945
|
+
placeholder="589"
|
|
946
|
+
.value=${this.formData.credentials?.employeeId || ""}
|
|
947
|
+
@input=${(e: Event) =>
|
|
948
|
+
this.updateForm("employeeId", (e.target as HTMLInputElement).value)}
|
|
949
|
+
/>
|
|
950
|
+
</div>
|
|
951
|
+
|
|
952
|
+
<div class="form-group">
|
|
953
|
+
<label class="form-label">Unit ID (optional)</label>
|
|
954
|
+
<input
|
|
955
|
+
class="form-input"
|
|
956
|
+
type="text"
|
|
957
|
+
placeholder="3"
|
|
958
|
+
.value=${this.formData.credentials?.unitId || ""}
|
|
959
|
+
@input=${(e: Event) =>
|
|
960
|
+
this.updateForm("unitId", (e.target as HTMLInputElement).value)}
|
|
961
|
+
/>
|
|
962
|
+
</div>
|
|
963
|
+
`
|
|
964
|
+
: null}
|
|
965
|
+
|
|
966
|
+
<div class="form-group">
|
|
967
|
+
<label class="form-label">Color</label>
|
|
968
|
+
<div class="color-picker">
|
|
969
|
+
${colors.map(
|
|
970
|
+
(color) => html`
|
|
971
|
+
<div
|
|
972
|
+
class="color-option ${this.formData.color === color
|
|
973
|
+
? "selected"
|
|
974
|
+
: ""}"
|
|
975
|
+
style="background: ${color}"
|
|
976
|
+
@click=${() => this.updateForm("color", color)}
|
|
977
|
+
></div>
|
|
978
|
+
`
|
|
979
|
+
)}
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
|
|
983
|
+
<div class="form-actions">
|
|
984
|
+
<button class="btn btn-secondary" @click=${this.cancelForm}>
|
|
985
|
+
Cancel
|
|
986
|
+
</button>
|
|
987
|
+
<button class="btn btn-primary" @click=${this.saveForm}>
|
|
988
|
+
${isEditing ? "Save" : "Add"}
|
|
989
|
+
</button>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
customElements.define("caldav-config", CalDAVConfigElement);
|
|
998
|
+
} catch (error) {
|
|
999
|
+
console.error("Failed to register custom element:", error);
|
|
1000
|
+
}
|