From eff6a0877184fcb37f53a78a3a7877da73bbb08c Mon Sep 17 00:00:00 2001 From: Bruno Bernardino Date: Tue, 19 Mar 2024 16:05:47 +0000 Subject: [PATCH] Calendar SQL and CRUD --- components/Header.tsx | 4 + db-migrations/004-calendar-app.pgsql | 107 ++++++++++ fresh.gen.ts | 10 + islands/calendar/Calendars.tsx | 299 ++++++++++++++++++++++++++ islands/calendar/MainCalendar.tsx | 2 +- islands/contacts/Contacts.tsx | 2 +- islands/news/Feeds.tsx | 2 +- lib/data/calendar.ts | 300 ++++++++------------------- lib/data/news.ts | 8 +- lib/types.ts | 2 - lib/utils.ts | 27 +++ routes/api/calendar/add.tsx | 39 ++++ routes/api/calendar/delete.tsx | 41 ++++ routes/api/calendar/update.tsx | 48 +++++ routes/api/news/delete-feed.tsx | 2 +- routes/calendars.tsx | 29 +++ 16 files changed, 703 insertions(+), 219 deletions(-) create mode 100644 db-migrations/004-calendar-app.pgsql create mode 100644 islands/calendar/Calendars.tsx create mode 100644 routes/api/calendar/add.tsx create mode 100644 routes/api/calendar/delete.tsx create mode 100644 routes/api/calendar/update.tsx create mode 100644 routes/calendars.tsx diff --git a/components/Header.tsx b/components/Header.tsx index 0fc9995..e53dc0a 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -70,6 +70,10 @@ export default function Header({ route, user }: Data) { pageLabel = 'Settings'; } + if (route.startsWith('/calendars')) { + pageLabel = 'Calendars'; + } + return ( <> diff --git a/db-migrations/004-calendar-app.pgsql b/db-migrations/004-calendar-app.pgsql new file mode 100644 index 0000000..d998775 --- /dev/null +++ b/db-migrations/004-calendar-app.pgsql @@ -0,0 +1,107 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + + +-- +-- Name: bewcloud_calendars; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.bewcloud_calendars ( + id uuid DEFAULT gen_random_uuid(), + user_id uuid DEFAULT gen_random_uuid(), + revision text NOT NULL, + name text NOT NULL, + color text NOT NULL, + is_visible boolean NOT NULL, + extra jsonb NOT NULL, + updated_at timestamp with time zone DEFAULT now(), + created_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.bewcloud_calendars OWNER TO postgres; + + +-- +-- Name: bewcloud_calendars bewcloud_calendars_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.bewcloud_calendars + ADD CONSTRAINT bewcloud_calendars_pkey PRIMARY KEY (id); + + +-- +-- Name: bewcloud_calendars bewcloud_calendars_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.bewcloud_calendars + ADD CONSTRAINT bewcloud_calendars_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.bewcloud_users(id); + + +-- +-- Name: TABLE bewcloud_calendars; Type: ACL; Schema: public; Owner: postgres +-- + +GRANT ALL ON TABLE public.bewcloud_calendars TO postgres; + + +-- +-- Name: bewcloud_calendar_events; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE public.bewcloud_calendar_events ( + id uuid DEFAULT gen_random_uuid(), + user_id uuid DEFAULT gen_random_uuid(), + calendar_id uuid DEFAULT gen_random_uuid(), + revision text NOT NULL, + title text NOT NULL, + start_date timestamp with time zone NOT NULL, + end_date timestamp with time zone NOT NULL, + is_all_day boolean NOT NULL, + status VARCHAR NOT NULL, + extra jsonb NOT NULL, + updated_at timestamp with time zone DEFAULT now(), + created_at timestamp with time zone DEFAULT now() +); + + +ALTER TABLE public.bewcloud_calendar_events OWNER TO postgres; + + +-- +-- Name: bewcloud_calendar_events bewcloud_calendar_events_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.bewcloud_calendar_events + ADD CONSTRAINT bewcloud_calendar_events_pkey PRIMARY KEY (id); + + +-- +-- Name: bewcloud_calendar_events bewcloud_calendar_events_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.bewcloud_calendar_events + ADD CONSTRAINT bewcloud_calendar_events_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.bewcloud_users(id); + + +-- +-- Name: bewcloud_calendar_events bewcloud_calendar_events_calendar_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.bewcloud_calendar_events + ADD CONSTRAINT bewcloud_calendar_events_calendar_id_fkey FOREIGN KEY (calendar_id) REFERENCES public.bewcloud_calendars(id); + + +-- +-- Name: TABLE bewcloud_calendar_events; Type: ACL; Schema: public; Owner: postgres +-- + +GRANT ALL ON TABLE public.bewcloud_calendar_events TO postgres; diff --git a/fresh.gen.ts b/fresh.gen.ts index 03ce848..d87feff 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -7,6 +7,9 @@ import * as $_well_known_carddav from './routes/.well-known/carddav.tsx'; import * as $_404 from './routes/_404.tsx'; import * as $_app from './routes/_app.tsx'; import * as $_middleware from './routes/_middleware.tsx'; +import * as $api_calendar_add from './routes/api/calendar/add.tsx'; +import * as $api_calendar_delete from './routes/api/calendar/delete.tsx'; +import * as $api_calendar_update from './routes/api/calendar/update.tsx'; import * as $api_contacts_add from './routes/api/contacts/add.tsx'; import * as $api_contacts_delete from './routes/api/contacts/delete.tsx'; import * as $api_contacts_get from './routes/api/contacts/get.tsx'; @@ -19,6 +22,7 @@ import * as $api_news_import_feeds from './routes/api/news/import-feeds.tsx'; import * as $api_news_mark_read from './routes/api/news/mark-read.tsx'; import * as $api_news_refresh_articles from './routes/api/news/refresh-articles.tsx'; import * as $calendar from './routes/calendar.tsx'; +import * as $calendars from './routes/calendars.tsx'; import * as $contacts from './routes/contacts.tsx'; import * as $contacts_contactId_ from './routes/contacts/[contactId].tsx'; import * as $dashboard from './routes/dashboard.tsx'; @@ -40,6 +44,7 @@ import * as $remote_php_davRoute_ from './routes/remote.php/[davRoute].tsx'; import * as $settings from './routes/settings.tsx'; import * as $signup from './routes/signup.tsx'; import * as $Settings from './islands/Settings.tsx'; +import * as $calendar_Calendars from './islands/calendar/Calendars.tsx'; import * as $calendar_MainCalendar from './islands/calendar/MainCalendar.tsx'; import * as $contacts_Contacts from './islands/contacts/Contacts.tsx'; import * as $contacts_ViewContact from './islands/contacts/ViewContact.tsx'; @@ -56,6 +61,9 @@ const manifest = { './routes/_404.tsx': $_404, './routes/_app.tsx': $_app, './routes/_middleware.tsx': $_middleware, + './routes/api/calendar/add.tsx': $api_calendar_add, + './routes/api/calendar/delete.tsx': $api_calendar_delete, + './routes/api/calendar/update.tsx': $api_calendar_update, './routes/api/contacts/add.tsx': $api_contacts_add, './routes/api/contacts/delete.tsx': $api_contacts_delete, './routes/api/contacts/get.tsx': $api_contacts_get, @@ -68,6 +76,7 @@ const manifest = { './routes/api/news/mark-read.tsx': $api_news_mark_read, './routes/api/news/refresh-articles.tsx': $api_news_refresh_articles, './routes/calendar.tsx': $calendar, + './routes/calendars.tsx': $calendars, './routes/contacts.tsx': $contacts, './routes/contacts/[contactId].tsx': $contacts_contactId_, './routes/dashboard.tsx': $dashboard, @@ -91,6 +100,7 @@ const manifest = { }, islands: { './islands/Settings.tsx': $Settings, + './islands/calendar/Calendars.tsx': $calendar_Calendars, './islands/calendar/MainCalendar.tsx': $calendar_MainCalendar, './islands/contacts/Contacts.tsx': $contacts_Contacts, './islands/contacts/ViewContact.tsx': $contacts_ViewContact, diff --git a/islands/calendar/Calendars.tsx b/islands/calendar/Calendars.tsx new file mode 100644 index 0000000..6e85273 --- /dev/null +++ b/islands/calendar/Calendars.tsx @@ -0,0 +1,299 @@ +import { useSignal } from '@preact/signals'; + +import { Calendar } from '/lib/types.ts'; +import { CALENDAR_COLOR_OPTIONS } from '/lib/utils.ts'; +import { RequestBody as AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/calendar/add.tsx'; +import { RequestBody as UpdateRequestBody, ResponseBody as UpdateResponseBody } from '/routes/api/calendar/update.tsx'; +import { RequestBody as DeleteRequestBody, ResponseBody as DeleteResponseBody } from '/routes/api/calendar/delete.tsx'; + +interface CalendarsProps { + initialCalendars: Calendar[]; +} + +export default function Calendars({ initialCalendars }: CalendarsProps) { + const isAdding = useSignal(false); + const isDeleting = useSignal(false); + const isSaving = useSignal(false); + const calendars = useSignal(initialCalendars); + const openCalendar = useSignal(null); + + async function onClickAddCalendar() { + if (isAdding.value) { + return; + } + + const name = (prompt(`What's the **name** for the new calendar?`) || '').trim(); + + if (!name) { + alert('A name is required for a new calendar!'); + return; + } + + isAdding.value = true; + + try { + const requestBody: AddRequestBody = { name }; + const response = await fetch(`/api/calendar/add`, { + method: 'POST', + body: JSON.stringify(requestBody), + }); + const result = await response.json() as AddResponseBody; + + if (!result.success) { + throw new Error('Failed to add calendar!'); + } + + calendars.value = [...result.newCalendars]; + } catch (error) { + console.error(error); + } + + isAdding.value = false; + } + + async function onClickDeleteCalendar(calendarId: string) { + if (confirm('Are you sure you want to delete this calendar and all its events?')) { + if (isDeleting.value) { + return; + } + + isDeleting.value = true; + + try { + const requestBody: DeleteRequestBody = { calendarId }; + const response = await fetch(`/api/calendar/delete`, { + method: 'POST', + body: JSON.stringify(requestBody), + }); + const result = await response.json() as DeleteResponseBody; + + if (!result.success) { + throw new Error('Failed to delete calendar!'); + } + + calendars.value = [...result.newCalendars]; + } catch (error) { + console.error(error); + } + + isDeleting.value = false; + } + } + + async function onClickSaveOpenCalendar() { + if (isSaving.value) { + return; + } + + if (!openCalendar.value?.id) { + alert('A calendar is required to update one!'); + return; + } + + if (!openCalendar.value?.name) { + alert('A name is required to update the calendar!'); + return; + } + + if (!openCalendar.value?.color) { + alert('A color is required to update the calendar!'); + return; + } + + isSaving.value = true; + + try { + const requestBody: UpdateRequestBody = { ...openCalendar.value }; + const response = await fetch(`/api/calendar/update`, { + method: 'POST', + body: JSON.stringify(requestBody), + }); + const result = await response.json() as UpdateResponseBody; + + if (!result.success) { + throw new Error('Failed to update calendar!'); + } + + calendars.value = [...result.newCalendars]; + } catch (error) { + console.error(error); + } + + isSaving.value = false; + openCalendar.value = null; + } + + return ( + <> +
+ View calendar +
+ +
+
+ +
+ + + + + + + + + + + {calendars.value.map((calendar) => ( + + + + + + + ))} + {calendars.value.length === 0 + ? ( + + + + ) + : null} + +
NameColorVisible?
+ {calendar.name} + + openCalendar.value = { ...calendar }} + > + + + {calendar.is_visible ? 'Yes' : 'No'} + + +
+
+
No calendars to show
+
+
+ + + {isDeleting.value + ? ( + <> + Deleting... + + ) + : null} + {isSaving.value + ? ( + <> + Saving... + + ) + : null} + {!isDeleting.value && !isSaving.value ? <>  : null} + +
+ +
+
+ +
+

Edit Calendar

+
+
+ + openCalendar.value = { ...openCalendar.value!, name: event.currentTarget.value }} + placeholder='Personal' + /> +
+
+ +
+ + + +
+
+
+ + + openCalendar.value = { ...openCalendar.value!, is_visible: event.currentTarget.checked }} + /> +
+
+
+ + +
+
+ + ); +} diff --git a/islands/calendar/MainCalendar.tsx b/islands/calendar/MainCalendar.tsx index 769b31e..99d7c56 100644 --- a/islands/calendar/MainCalendar.tsx +++ b/islands/calendar/MainCalendar.tsx @@ -398,7 +398,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
- Manage calendars + Manage calendars
- +
diff --git a/islands/news/Feeds.tsx b/islands/news/Feeds.tsx index 860cff7..6cbacf9 100644 --- a/islands/news/Feeds.tsx +++ b/islands/news/Feeds.tsx @@ -283,7 +283,7 @@ export default function Feeds({ initialFeeds }: FeedsProps) {
-
First Name
+
diff --git a/lib/data/calendar.ts b/lib/data/calendar.ts index 9f0228f..4c93a31 100644 --- a/lib/data/calendar.ts +++ b/lib/data/calendar.ts @@ -1,173 +1,56 @@ -// import Database, { sql } from '/lib/interfaces/database.ts'; +import Database, { sql } from '/lib/interfaces/database.ts'; import { Calendar, CalendarEvent } from '/lib/types.ts'; +import { CALENDAR_COLOR_OPTIONS, getRandomItem } from '/lib/utils.ts'; +// import { getUserById } from './user.ts'; -// const db = new Database(); +const db = new Database(); -// TODO: Build this export async function getCalendars(userId: string): Promise { - // TODO: Remove this - await new Promise((resolve) => setTimeout(() => resolve(true), 1)); + const calendars = await db.query( + sql`SELECT * FROM "bewcloud_calendars" WHERE "user_id" = $1 ORDER BY "created_at" ASC`, + [ + userId, + ], + ); - return [ - { - id: 'family-1', - user_id: userId, - name: 'Family', - color: 'bg-purple-500', - is_visible: true, - revision: 'fake-rev', - extra: { - default_transparency: 'opaque', - }, - updated_at: new Date(), - created_at: new Date(), - }, - { - id: 'personal-1', - user_id: userId, - name: 'Personal', - color: 'bg-sky-600', - is_visible: true, - revision: 'fake-rev', - extra: { - default_transparency: 'opaque', - }, - updated_at: new Date(), - created_at: new Date(), - }, - { - id: 'house-chores-1', - user_id: userId, - name: 'House Chores', - color: 'bg-red-700', - is_visible: true, - revision: 'fake-rev', - extra: { - default_transparency: 'opaque', - }, - updated_at: new Date(), - created_at: new Date(), - }, - ]; + return calendars; } -// TODO: Build this export async function getCalendarEvents(userId: string, calendarIds: string[]): Promise { - // TODO: Remove this - await new Promise((resolve) => setTimeout(() => resolve(true), 1)); + const calendarEvents = await db.query( + sql`SELECT * FROM "bewcloud_calendar_events" WHERE "user_id" = $1 AND "calendar_id" = ANY($2) ORDER BY "start_date" ASC`, + [ + userId, + calendarIds, + ], + ); - const now = new Date(); - const today = now.toISOString().substring(0, 10); - const tomorrow = new Date(new Date(now).setDate(now.getDate() + 1)).toISOString().substring(0, 10); - const twoDaysFromNow = new Date(new Date(now).setDate(now.getDate() + 2)).toISOString().substring(0, 10); - - const calendarEvents = [ - { - id: 'event-1', - user_id: userId, - calendar_id: 'family-1', - revision: 'fake-rev', - title: 'Dentist', - start_date: new Date(`${today}T14:00:00.000Z`), - end_date: new Date(`${today}T15:00:00.000Z`), - is_all_day: false, - status: 'scheduled', - extra: { - organizer_email: 'user@example.com', - transparency: 'default', - }, - updated_at: new Date(), - created_at: new Date(), - }, - { - id: 'event-2', - user_id: userId, - calendar_id: 'family-1', - revision: 'fake-rev', - title: 'Dermatologist', - start_date: new Date(`${today}T16:30:00.000Z`), - end_date: new Date(`${today}T17:30:00.000Z`), - is_all_day: false, - status: 'scheduled', - extra: { - organizer_email: 'user@example.com', - transparency: 'default', - }, - updated_at: new Date(), - created_at: new Date(), - }, - { - id: 'event-3', - user_id: userId, - calendar_id: 'house-chores-1', - revision: 'fake-rev', - title: 'Vacuum', - start_date: new Date(`${tomorrow}T15:00:00.000Z`), - end_date: new Date(`${tomorrow}T16:00:00.000Z`), - is_all_day: false, - status: 'scheduled', - extra: { - organizer_email: 'user@example.com', - transparency: 'default', - }, - updated_at: new Date(), - created_at: new Date(), - }, - { - id: 'event-4', - user_id: userId, - calendar_id: 'personal-1', - revision: 'fake-rev', - title: 'Schedule server updates', - start_date: new Date(`${twoDaysFromNow}T09:00:00.000Z`), - end_date: new Date(`${twoDaysFromNow}T21:00:00.000Z`), - is_all_day: true, - status: 'scheduled', - extra: { - organizer_email: 'user@example.com', - transparency: 'default', - }, - updated_at: new Date(), - created_at: new Date(), - }, - ] as const; - - return calendarEvents.filter((calendarEvent) => calendarIds.includes(calendarEvent.calendar_id)); + return calendarEvents; } -// TODO: Build this export async function getCalendarEvent(id: string, calendarId: string, userId: string): Promise { - // TODO: Build this - // const calendarEvents = await db.query( - // sql`SELECT * FROM "bewcloud_calendar_events" WHERE "id" = $1 AND "calendar_id" = $2 AND "user_id" = $3 LIMIT 1`, - // [ - // id, - // calendarId, - // userId, - // ], - // ); + const calendarEvents = await db.query( + sql`SELECT * FROM "bewcloud_calendar_events" WHERE "id" = $1 AND "calendar_id" = $2 AND "user_id" = $3 LIMIT 1`, + [ + id, + calendarId, + userId, + ], + ); - // return calendarEvents[0]; - const calendarEvents = await getCalendarEvents(userId, [calendarId]); - - return calendarEvents.find((calendarEvent) => calendarEvent.id === id)!; + return calendarEvents[0]; } export async function getCalendar(id: string, userId: string) { - // TODO: Build this - // const calendars = await db.query( - // sql`SELECT * FROM "bewcloud_calendars" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`, - // [ - // id, - // userId, - // ], - // ); + const calendars = await db.query( + sql`SELECT * FROM "bewcloud_calendars" WHERE "id" = $1 AND "user_id" = $2 LIMIT 1`, + [ + id, + userId, + ], + ); - // return calendars[0]; - - const calendars = await getCalendars(userId); - - return calendars.find((calendar) => calendar.id === id)!; + return calendars[0]; } export async function createCalendar(userId: string, name: string, color?: string) { @@ -177,30 +60,27 @@ export async function createCalendar(userId: string, name: string, color?: strin const revision = crypto.randomUUID(); - // TODO: Build this - // const newCalendar = (await db.query( - // sql`INSERT INTO "bewcloud_calendars" ( - // "user_id", - // "revision", - // "name", - // "color", - // "extra" - // ) VALUES ($1, $2, $3, $4, $5) - // RETURNING *`, - // [ - // userId, - // revision, - // name, - // color, - // JSON.stringify(extra), - // ], - // ))[0]; + const newColor = color || getRandomItem(CALENDAR_COLOR_OPTIONS); - // TODO: Generate new, non-existing color - const newColor = color || 'bg-green-600'; - - const calendars = await getCalendars(userId); - const newCalendar = { ...calendars[0], id: crypto.randomUUID(), revision, extra, name, color: newColor }; + const newCalendar = (await db.query( + sql`INSERT INTO "bewcloud_calendars" ( + "user_id", + "revision", + "name", + "color", + "is_visible", + "extra" + ) VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *`, + [ + userId, + revision, + name, + newColor, + true, + JSON.stringify(extra), + ], + ))[0]; return newCalendar; } @@ -208,43 +88,43 @@ export async function createCalendar(userId: string, name: string, color?: strin export async function updateCalendar(calendar: Calendar) { const revision = crypto.randomUUID(); - // TODO: Build this - // await db.query( - // sql`UPDATE "bewcloud_calendars" SET - // "revision" = $3, - // "name" = $4, - // "color" = $5, - // "extra" = $6, - // "updated_at" = now() - // WHERE "id" = $1 AND "revision" = $2`, - // [ - // calendar.id, - // calendar.revision, - // revision, - // calendar.name, - // calendar.color, - // JSON.stringify(calendar.extra), - // ], - // ); - - calendar.revision = revision; - - // TODO: Remove this - await new Promise((resolve) => setTimeout(() => resolve(true), 1)); + await db.query( + sql`UPDATE "bewcloud_calendars" SET + "revision" = $3, + "name" = $4, + "color" = $5, + "is_visible" = $6, + "extra" = $7, + "updated_at" = now() + WHERE "id" = $1 AND "revision" = $2`, + [ + calendar.id, + calendar.revision, + revision, + calendar.name, + calendar.color, + calendar.is_visible, + JSON.stringify(calendar.extra), + ], + ); } -export async function deleteCalendar(_id: string, _userId: string) { - // TODO: Build this - // await db.query( - // sql`DELETE FROM "bewcloud_calendars" WHERE "id" = $1 AND "user_id" = $2`, - // [ - // id, - // userId, - // ], - // ); +export async function deleteCalendar(id: string, userId: string) { + await db.query( + sql`DELETE FROM "bewcloud_calendar_events" WHERE "calendar_id" = $1 AND "user_id" = $2`, + [ + id, + userId, + ], + ); - // TODO: Remove this - await new Promise((resolve) => setTimeout(() => resolve(true), 1)); + await db.query( + sql`DELETE FROM "bewcloud_calendars" WHERE "id" = $1 AND "user_id" = $2`, + [ + id, + userId, + ], + ); } // TODO: When creating, updating, or deleting events, also update the calendar's revision diff --git a/lib/data/news.ts b/lib/data/news.ts index 99e14fe..90a0303 100644 --- a/lib/data/news.ts +++ b/lib/data/news.ts @@ -105,18 +105,20 @@ export async function updateNewsFeed(newsFeed: NewsFeed) { ); } -export async function deleteNewsFeed(id: string) { +export async function deleteNewsFeed(id: string, userId: string) { await db.query( - sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1`, + sql`DELETE FROM "bewcloud_news_feed_articles" WHERE "feed_id" = $1 AND "user_id" = $2`, [ id, + userId, ], ); await db.query( - sql`DELETE FROM "bewcloud_news_feeds" WHERE "id" = $1`, + sql`DELETE FROM "bewcloud_news_feeds" WHERE "id" = $1 AND "user_id" = $2`, [ id, + userId, ], ); } diff --git a/lib/types.ts b/lib/types.ts index 65a30d8..8174276 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -130,7 +130,6 @@ export interface ContactField { type: ContactFieldType; } -// TODO: Finish (more fields) export interface Calendar { id: string; user_id: string; @@ -148,7 +147,6 @@ export interface Calendar { created_at: Date; } -// TODO: Finish (more fields) export interface CalendarEvent { id: string; user_id: string; diff --git a/lib/utils.ts b/lib/utils.ts index b2416a6..54da672 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -23,6 +23,29 @@ export const DAV_RESPONSE_HEADER = '1, 2, 3, 4, addressbook, calendar-access'; // 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-trashbin, nc-calendar-webcal-cache, calendarserver-subscribed, oc-resource-sharing, oc-calendar-publishing, calendarserver-sharing, addressbook, nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar // 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-trashbin, nc-calendar-webcal-cache, calendarserver-subscribed, oc-resource-sharing, oc-calendar-publishing, calendarserver-sharing, nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar +export const CALENDAR_COLOR_OPTIONS = [ + 'bg-red-700', + 'bg-red-950', + 'bg-orange-700', + 'bg-orange-950', + 'bg-amber-700', + 'bg-yellow-800', + 'bg-lime-700', + 'bg-lime-950', + 'bg-green-700', + 'bg-emerald-800', + 'bg-teal-700', + 'bg-cyan-700', + 'bg-sky-800', + 'bg-blue-900', + 'bg-indigo-700', + 'bg-violet-700', + 'bg-purple-800', + 'bg-fuchsia-700', + 'bg-pink-800', + 'bg-rose-700', +] as const; + export function isRunningLocally(request: Request) { return request.url.includes('localhost'); } @@ -805,3 +828,7 @@ export function getDaysForWeek( return days; } + +export function getRandomItem(items: Readonly>): T { + return items[Math.floor(Math.random() * items.length)]; +} diff --git a/routes/api/calendar/add.tsx b/routes/api/calendar/add.tsx new file mode 100644 index 0000000..d217016 --- /dev/null +++ b/routes/api/calendar/add.tsx @@ -0,0 +1,39 @@ +import { Handlers } from 'fresh/server.ts'; + +import { Calendar, FreshContextState } from '/lib/types.ts'; +import { createCalendar, getCalendars } from '/lib/data/calendar.ts'; + +interface Data {} + +export interface RequestBody { + name: string; +} + +export interface ResponseBody { + success: boolean; + newCalendars: Calendar[]; +} + +export const handler: Handlers = { + async POST(request, context) { + if (!context.state.user) { + return new Response('Unauthorized', { status: 401 }); + } + + const requestBody = await request.clone().json() as RequestBody; + + if (requestBody.name) { + const newCalendar = await createCalendar(context.state.user.id, requestBody.name); + + if (!newCalendar) { + return new Response('Not found', { status: 404 }); + } + } + + const newCalendars = await getCalendars(context.state.user.id); + + const responseBody: ResponseBody = { success: true, newCalendars }; + + return new Response(JSON.stringify(responseBody)); + }, +}; diff --git a/routes/api/calendar/delete.tsx b/routes/api/calendar/delete.tsx new file mode 100644 index 0000000..53086b1 --- /dev/null +++ b/routes/api/calendar/delete.tsx @@ -0,0 +1,41 @@ +import { Handlers } from 'fresh/server.ts'; + +import { Calendar, FreshContextState } from '/lib/types.ts'; +import { deleteCalendar, getCalendar, getCalendars } from '/lib/data/calendar.ts'; + +interface Data {} + +export interface RequestBody { + calendarId: string; +} + +export interface ResponseBody { + success: boolean; + newCalendars: Calendar[]; +} + +export const handler: Handlers = { + async POST(request, context) { + if (!context.state.user) { + return new Response('Unauthorized', { status: 401 }); + } + + const requestBody = await request.clone().json() as RequestBody; + + if (requestBody.calendarId) { + const calendar = await getCalendar(requestBody.calendarId, context.state.user.id); + + if (!calendar) { + return new Response('Not found', { status: 404 }); + } + + await deleteCalendar(requestBody.calendarId, context.state.user.id); + } + + const newCalendars = await getCalendars(context.state.user.id); + + const responseBody: ResponseBody = { success: true, newCalendars }; + + return new Response(JSON.stringify(responseBody)); + }, +}; diff --git a/routes/api/calendar/update.tsx b/routes/api/calendar/update.tsx new file mode 100644 index 0000000..9045ec9 --- /dev/null +++ b/routes/api/calendar/update.tsx @@ -0,0 +1,48 @@ +import { Handlers } from 'fresh/server.ts'; + +import { Calendar, FreshContextState } from '/lib/types.ts'; +import { getCalendar, getCalendars, updateCalendar } from '/lib/data/calendar.ts'; + +interface Data {} + +export interface RequestBody { + id: string; + name: string; + color: string; + is_visible: boolean; +} + +export interface ResponseBody { + success: boolean; + newCalendars: Calendar[]; +} + +export const handler: Handlers = { + async POST(request, context) { + if (!context.state.user) { + return new Response('Unauthorized', { status: 401 }); + } + + const requestBody = await request.clone().json() as RequestBody; + + if (requestBody.id) { + const calendar = await getCalendar(requestBody.id, context.state.user.id); + + if (!calendar) { + return new Response('Not found', { status: 404 }); + } + + calendar.name = requestBody.name; + calendar.color = requestBody.color; + calendar.is_visible = requestBody.is_visible; + + await updateCalendar(calendar); + } + + const newCalendars = await getCalendars(context.state.user.id); + + const responseBody: ResponseBody = { success: true, newCalendars }; + + return new Response(JSON.stringify(responseBody)); + }, +}; diff --git a/routes/api/news/delete-feed.tsx b/routes/api/news/delete-feed.tsx index 3eb82ea..c04475f 100644 --- a/routes/api/news/delete-feed.tsx +++ b/routes/api/news/delete-feed.tsx @@ -29,7 +29,7 @@ export const handler: Handlers = { return new Response('Not found', { status: 404 }); } - await deleteNewsFeed(requestBody.feedId); + await deleteNewsFeed(requestBody.feedId, context.state.user.id); } const newFeeds = await getNewsFeeds(context.state.user.id); diff --git a/routes/calendars.tsx b/routes/calendars.tsx new file mode 100644 index 0000000..d0c17d7 --- /dev/null +++ b/routes/calendars.tsx @@ -0,0 +1,29 @@ +import { Handlers, PageProps } from 'fresh/server.ts'; + +import { Calendar, FreshContextState } from '/lib/types.ts'; +import { getCalendars } from '/lib/data/calendar.ts'; +import Calendars from '/islands/calendar/Calendars.tsx'; + +interface Data { + userCalendars: Calendar[]; +} + +export const handler: Handlers = { + async GET(request, context) { + if (!context.state.user) { + return new Response('Redirect', { status: 303, headers: { 'Location': `/login` } }); + } + + const userCalendars = await getCalendars(context.state.user.id); + + return await context.render({ userCalendars }); + }, +}; + +export default function CalendarsPage({ data }: PageProps) { + return ( +
+ +
+ ); +}
Title & URL