@axium/calendar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +157 -0
- package/README.md +1 -0
- package/db.json +76 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +12 -0
- package/dist/common.d.ts +461 -0
- package/dist/common.js +194 -0
- package/dist/hooks.d.ts +3 -0
- package/dist/hooks.js +7 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +208 -0
- package/lib/Select.svelte +127 -0
- package/lib/index.ts +1 -0
- package/lib/tsconfig.json +13 -0
- package/package.json +62 -0
- package/routes/calendar/+page.svelte +610 -0
- package/routes/calendar/+page.ts +31 -0
- package/routes/tsconfig.json +12 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { from as aclFrom, userHasAccess } from '@axium/server/acl';
|
|
2
|
+
import { authRequestForItem, checkAuthForUser, requireSession } from '@axium/server/auth';
|
|
3
|
+
import { database } from '@axium/server/database';
|
|
4
|
+
import { error, parseBody, parseSearch, withError } from '@axium/server/requests';
|
|
5
|
+
import { addRoute } from '@axium/server/routes';
|
|
6
|
+
import { sql } from 'kysely';
|
|
7
|
+
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import { Attendee, CalendarInit, EventFilter, EventInit } from './common.js';
|
|
10
|
+
addRoute({
|
|
11
|
+
path: '/api/users/:id/calendars',
|
|
12
|
+
params: { id: z.uuid() },
|
|
13
|
+
async GET(request, { id: userId }) {
|
|
14
|
+
const { user } = await checkAuthForUser(request, userId);
|
|
15
|
+
return await database
|
|
16
|
+
.selectFrom('calendars')
|
|
17
|
+
.selectAll()
|
|
18
|
+
.select(aclFrom('calendars'))
|
|
19
|
+
.where(userHasAccess('calendars', user))
|
|
20
|
+
.execute()
|
|
21
|
+
.catch(withError('Could not get calendars'));
|
|
22
|
+
},
|
|
23
|
+
async PUT(request, { id: userId }) {
|
|
24
|
+
const init = await parseBody(request, CalendarInit);
|
|
25
|
+
await checkAuthForUser(request, userId);
|
|
26
|
+
return await database
|
|
27
|
+
.insertInto('calendars')
|
|
28
|
+
.values({ ...init, userId })
|
|
29
|
+
.returningAll()
|
|
30
|
+
.executeTakeFirstOrThrow()
|
|
31
|
+
.catch(withError('Could not create calendar'));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
addRoute({
|
|
35
|
+
path: '/api/calendars/:id',
|
|
36
|
+
params: { id: z.uuid() },
|
|
37
|
+
async GET(request, { id }) {
|
|
38
|
+
const { item } = await authRequestForItem(request, 'calendars', id, { read: true });
|
|
39
|
+
return item;
|
|
40
|
+
},
|
|
41
|
+
async PATCH(request, { id }) {
|
|
42
|
+
const body = await parseBody(request, CalendarInit);
|
|
43
|
+
await authRequestForItem(request, 'calendars', id, { edit: true });
|
|
44
|
+
return await database
|
|
45
|
+
.updateTable('calendars')
|
|
46
|
+
.set(body)
|
|
47
|
+
.where('id', '=', id)
|
|
48
|
+
.returningAll()
|
|
49
|
+
.executeTakeFirstOrThrow()
|
|
50
|
+
.catch(withError('Could not update calendar'));
|
|
51
|
+
},
|
|
52
|
+
async DELETE(request, { id }) {
|
|
53
|
+
await authRequestForItem(request, 'calendars', id, { manage: true });
|
|
54
|
+
return await database
|
|
55
|
+
.deleteFrom('calendars')
|
|
56
|
+
.where('id', '=', id)
|
|
57
|
+
.returningAll()
|
|
58
|
+
.executeTakeFirstOrThrow()
|
|
59
|
+
.catch(withError('Could not delete calendar'));
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
function withAttendees(eb) {
|
|
63
|
+
return jsonArrayFrom(eb.selectFrom('attendees').selectAll().whereRef('eventId', '=', 'events.id').$narrowType()).as('attendees');
|
|
64
|
+
}
|
|
65
|
+
function withCalendar(eb) {
|
|
66
|
+
return jsonObjectFrom(eb.selectFrom('calendars').selectAll().whereRef('id', '=', 'events.calId'))
|
|
67
|
+
.$castTo() // json dates
|
|
68
|
+
.as('calendar');
|
|
69
|
+
}
|
|
70
|
+
addRoute({
|
|
71
|
+
path: '/api/calendars/:id/events',
|
|
72
|
+
params: { id: z.uuid() },
|
|
73
|
+
async GET(request, { id }) {
|
|
74
|
+
const filter = parseSearch(request, EventFilter);
|
|
75
|
+
const { item: calendar } = await authRequestForItem(request, 'calendars', id, { read: true });
|
|
76
|
+
const events = await database
|
|
77
|
+
.selectFrom('events')
|
|
78
|
+
.selectAll()
|
|
79
|
+
.select(withAttendees)
|
|
80
|
+
.where('calId', '=', id)
|
|
81
|
+
.where(sql `(${sql.ref('start')}, ${sql.ref('end')}) OVERLAPS (${sql.val(filter.start)}, ${sql.val(filter.end)})`)
|
|
82
|
+
.limit(1000)
|
|
83
|
+
.execute()
|
|
84
|
+
.catch(withError('Could not get events'));
|
|
85
|
+
for (const event of events)
|
|
86
|
+
event.calendar = calendar;
|
|
87
|
+
return events;
|
|
88
|
+
},
|
|
89
|
+
async PUT(request, { id }) {
|
|
90
|
+
const { attendees: attendeesInit = [], sendEmails, recurrenceUpdateAll, ...init } = await parseBody(request, EventInit);
|
|
91
|
+
const { item: calendar } = await authRequestForItem(request, 'calendars', id, { edit: true });
|
|
92
|
+
const tx = await database.startTransaction().execute();
|
|
93
|
+
try {
|
|
94
|
+
const event = await tx
|
|
95
|
+
.insertInto('events')
|
|
96
|
+
.values({ ...init, calId: id })
|
|
97
|
+
.returningAll()
|
|
98
|
+
.executeTakeFirstOrThrow()
|
|
99
|
+
.catch(withError('Could not create event'));
|
|
100
|
+
const attendees = Attendee.array().parse(attendeesInit.length
|
|
101
|
+
? await tx
|
|
102
|
+
.insertInto('attendees')
|
|
103
|
+
.values(attendeesInit.map(a => ({ ...a, eventId: event.id })))
|
|
104
|
+
.returningAll()
|
|
105
|
+
.execute()
|
|
106
|
+
: []);
|
|
107
|
+
await tx.commit().execute();
|
|
108
|
+
return Object.assign(event, { attendees, calendar });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
await tx.rollback().execute();
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
addRoute({
|
|
117
|
+
path: '/api/events/:id',
|
|
118
|
+
params: { id: z.uuid() },
|
|
119
|
+
async GET(request, { id }) {
|
|
120
|
+
const { userId } = await requireSession(request);
|
|
121
|
+
const event = await database
|
|
122
|
+
.selectFrom('events')
|
|
123
|
+
.selectAll()
|
|
124
|
+
.select(withAttendees)
|
|
125
|
+
.select(withCalendar)
|
|
126
|
+
.where('id', '=', id)
|
|
127
|
+
.executeTakeFirstOrThrow()
|
|
128
|
+
.catch(withError('Event does not exist', 404));
|
|
129
|
+
if (event.calendar.userId != userId && event.attendees.every(a => a.userId != userId))
|
|
130
|
+
error(403, 'You do not have access to this event');
|
|
131
|
+
return event;
|
|
132
|
+
},
|
|
133
|
+
async PATCH(request, { id }) {
|
|
134
|
+
const { attendees: attendeesInit = [], sendEmails, recurrenceUpdateAll, ...init } = await parseBody(request, EventInit);
|
|
135
|
+
const { calId } = await database
|
|
136
|
+
.selectFrom('events')
|
|
137
|
+
.select('calId')
|
|
138
|
+
.where('id', '=', id)
|
|
139
|
+
.executeTakeFirstOrThrow()
|
|
140
|
+
.catch(withError('Event does not exist', 404));
|
|
141
|
+
await authRequestForItem(request, 'calendars', calId, { edit: true });
|
|
142
|
+
if (calId != init.calId)
|
|
143
|
+
await authRequestForItem(request, 'calendars', init.calId, { edit: true });
|
|
144
|
+
const tx = await database.startTransaction().execute();
|
|
145
|
+
try {
|
|
146
|
+
const event = await tx
|
|
147
|
+
.updateTable('events')
|
|
148
|
+
.set(init)
|
|
149
|
+
.where('id', '=', id)
|
|
150
|
+
.returningAll()
|
|
151
|
+
.returning(withAttendees)
|
|
152
|
+
.returning(withCalendar)
|
|
153
|
+
.executeTakeFirstOrThrow()
|
|
154
|
+
.catch(withError('Could not update event'));
|
|
155
|
+
if (calId != init.calId) {
|
|
156
|
+
await tx
|
|
157
|
+
.updateTable('events')
|
|
158
|
+
.set({ calId: init.calId })
|
|
159
|
+
.where('recurrenceId', '=', id)
|
|
160
|
+
.execute()
|
|
161
|
+
.catch(withError('Failed to update calendar for dependent events'));
|
|
162
|
+
}
|
|
163
|
+
const attendeeInitMap = new Map(attendeesInit.map(a => [a.email, a]));
|
|
164
|
+
for (const attendee of event.attendees) {
|
|
165
|
+
const userInit = attendee.userId && attendeeInitMap.get(attendee.userId);
|
|
166
|
+
const init = attendeeInitMap.get(attendee.email);
|
|
167
|
+
if (!userInit && !init)
|
|
168
|
+
continue;
|
|
169
|
+
/**
|
|
170
|
+
* @todo update attendees
|
|
171
|
+
* Need to handle:
|
|
172
|
+
* - Changing name
|
|
173
|
+
*/
|
|
174
|
+
}
|
|
175
|
+
await tx.commit().execute();
|
|
176
|
+
return event;
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
await tx.rollback().execute();
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
async DELETE(request, { id }) {
|
|
184
|
+
const { calId } = await database
|
|
185
|
+
.selectFrom('events')
|
|
186
|
+
.select('calId')
|
|
187
|
+
.where('id', '=', id)
|
|
188
|
+
.executeTakeFirstOrThrow()
|
|
189
|
+
.catch(withError('Event does not exist', 404));
|
|
190
|
+
await authRequestForItem(request, 'calendars', calId, { manage: true });
|
|
191
|
+
const tx = await database.startTransaction().execute();
|
|
192
|
+
try {
|
|
193
|
+
const event = await tx
|
|
194
|
+
.deleteFrom('events')
|
|
195
|
+
.returningAll()
|
|
196
|
+
.returning(withAttendees)
|
|
197
|
+
.returning(withCalendar)
|
|
198
|
+
.where('id', '=', id)
|
|
199
|
+
.executeTakeFirstOrThrow();
|
|
200
|
+
await tx.commit().execute();
|
|
201
|
+
return event;
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
await tx.rollback().execute();
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { weekOfYear, type EventFilter } from '@axium/calendar/common';
|
|
3
|
+
import { Icon } from '@axium/client/components';
|
|
4
|
+
import { SvelteDate } from 'svelte/reactivity';
|
|
5
|
+
|
|
6
|
+
let { start = $bindable(), end = $bindable() }: Required<EventFilter> = $props();
|
|
7
|
+
|
|
8
|
+
const today = new Date();
|
|
9
|
+
today.setHours(0, 0, 0, 0);
|
|
10
|
+
|
|
11
|
+
let view = new SvelteDate(start);
|
|
12
|
+
$effect(() => {
|
|
13
|
+
view.setTime(start.getTime());
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const firstOfMonth = $derived(new Date(view.getFullYear(), view.getMonth(), 1));
|
|
17
|
+
const firstWeekOfMonth = $derived(weekOfYear(firstOfMonth, true));
|
|
18
|
+
const lastOfMonth = $derived(new Date(view.getFullYear(), view.getMonth() + 1, 0));
|
|
19
|
+
const sameMonth = (d: Date) => view.getFullYear() == d.getFullYear() && view.getMonth() == d.getMonth();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="CaldendarSelect">
|
|
23
|
+
<div class="bar">
|
|
24
|
+
<span class="label">{view.toLocaleString('default', { month: 'long', year: 'numeric' })}</span>
|
|
25
|
+
<button style:display="contents" onclick={() => view.setMonth(view.getMonth() - 1)}><Icon i="chevron-left" /></button>
|
|
26
|
+
<button style:display="contents" onclick={() => view.setMonth(view.getMonth() + 1)}><Icon i="chevron-right" /></button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="month-grid">
|
|
30
|
+
<div></div>
|
|
31
|
+
{#each ['S', 'M', 'T', 'W', 'T', 'F', 'S'] as day, i}
|
|
32
|
+
<div class={['d-of-w', sameMonth(today) && today.getDay() == i && 'current']}>{day}</div>
|
|
33
|
+
{/each}
|
|
34
|
+
|
|
35
|
+
<div class={['w-of-y', firstWeekOfMonth == weekOfYear(today, true) && 'current']}>{firstWeekOfMonth}</div>
|
|
36
|
+
|
|
37
|
+
{#each { length: firstOfMonth.getDay() }}
|
|
38
|
+
<div class="empty"></div>
|
|
39
|
+
{/each}
|
|
40
|
+
{#each { length: lastOfMonth.getDate() }, i}
|
|
41
|
+
{@const day = i + 1}
|
|
42
|
+
{@const year = view.getFullYear()}
|
|
43
|
+
{@const month = view.getMonth()}
|
|
44
|
+
{@const date = new Date(year, month, day)}
|
|
45
|
+
{@const WofY = weekOfYear(date, true)}
|
|
46
|
+
{#if date.getDay() == 0 && WofY != firstWeekOfMonth}
|
|
47
|
+
<div class={['w-of-y', WofY == weekOfYear(today, true) && 'current']}>{WofY}</div>
|
|
48
|
+
{/if}
|
|
49
|
+
<div
|
|
50
|
+
class={[
|
|
51
|
+
'grid-day',
|
|
52
|
+
sameMonth(today) && day == today.getDate() && 'today',
|
|
53
|
+
sameMonth(start) && day >= start.getDate() && day < end.getDate() && 'selected',
|
|
54
|
+
]}
|
|
55
|
+
onclick={() => {
|
|
56
|
+
start.setFullYear(year);
|
|
57
|
+
start.setMonth(month);
|
|
58
|
+
start.setDate(day - date.getDay());
|
|
59
|
+
end.setFullYear(year);
|
|
60
|
+
end.setMonth(month);
|
|
61
|
+
end.setDate(day - date.getDay() + 7);
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{day}
|
|
65
|
+
</div>
|
|
66
|
+
{/each}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.CalendarSelect {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.bar {
|
|
78
|
+
display: grid;
|
|
79
|
+
gap: 1em;
|
|
80
|
+
grid-template-columns: 1fr 1em 1em;
|
|
81
|
+
align-items: center;
|
|
82
|
+
padding-bottom: 1em;
|
|
83
|
+
font-weight: bold;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.month-grid {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: repeat(8, 1.5em);
|
|
89
|
+
grid-template-rows: repeat(7, 1.5em);
|
|
90
|
+
gap: 0.25em;
|
|
91
|
+
|
|
92
|
+
> div {
|
|
93
|
+
user-select: none;
|
|
94
|
+
padding: 0.25em;
|
|
95
|
+
width: 1.5em;
|
|
96
|
+
height: 1.5em;
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
text-align: center;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.grid-day {
|
|
104
|
+
border-radius: 0.25em;
|
|
105
|
+
|
|
106
|
+
&:hover {
|
|
107
|
+
background-color: var(--bg-accent);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&.today {
|
|
111
|
+
border: 1px solid var(--border-accent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&.selected {
|
|
115
|
+
color: var(--fg-accent);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.d-of-w,
|
|
120
|
+
.w-of-y {
|
|
121
|
+
font-weight: bold;
|
|
122
|
+
&.current {
|
|
123
|
+
color: var(--fg-strong);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Select } from './Select.svelte';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "..",
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"module": "preserve",
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"types": ["@sveltejs/kit"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["**/*.svelte", "**/*.ts"],
|
|
11
|
+
"exclude": [],
|
|
12
|
+
"references": [{ "path": ".." }]
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@axium/calendar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
|
+
"description": "Calendar for Axium",
|
|
6
|
+
"funding": {
|
|
7
|
+
"type": "individual",
|
|
8
|
+
"url": "https://github.com/sponsors/james-pre"
|
|
9
|
+
},
|
|
10
|
+
"license": "LGPL-3.0-or-later",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/james-pre/axium.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/james-pre/axium#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/james-pre/axium/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/index.js",
|
|
24
|
+
"./*": "./dist/*.js",
|
|
25
|
+
"./components": "./lib/index.js",
|
|
26
|
+
"./components/*": "./lib/*.svelte"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"lib",
|
|
31
|
+
"routes",
|
|
32
|
+
"db.json"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@axium/client": ">=0.14.3",
|
|
39
|
+
"@axium/core": ">=0.19.0",
|
|
40
|
+
"@axium/server": ">=0.36.0",
|
|
41
|
+
"@sveltejs/kit": "^2.27.3",
|
|
42
|
+
"utilium": "^2.6.3"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"zod": "^4.0.5"
|
|
46
|
+
},
|
|
47
|
+
"axium": {
|
|
48
|
+
"server": {
|
|
49
|
+
"routes": "routes",
|
|
50
|
+
"hooks": "./dist/hooks.js",
|
|
51
|
+
"db": "./db.json"
|
|
52
|
+
},
|
|
53
|
+
"apps": [
|
|
54
|
+
{
|
|
55
|
+
"id": "calendar",
|
|
56
|
+
"name": "Calendar",
|
|
57
|
+
"icon": "calendar"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"update_checks": true
|
|
61
|
+
}
|
|
62
|
+
}
|