diff --git a/components/calendar/CalendarViewWeek.tsx b/components/calendar/CalendarViewWeek.tsx index bfd5c95..73b4d93 100644 --- a/components/calendar/CalendarViewWeek.tsx +++ b/components/calendar/CalendarViewWeek.tsx @@ -15,7 +15,7 @@ export default function CalendarViewWeek( const today = new Date().toISOString().substring(0, 10); const hourFormat = new Intl.DateTimeFormat('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' }); - const weekDayFormat = new Intl.DateTimeFormat('en-GB', { weekday: 'short' }); + const weekDayFormat = new Intl.DateTimeFormat('en-GB', { weekday: 'short', day: 'numeric', month: '2-digit' }); const days = getDaysForWeek(new Date(startDate)); diff --git a/components/calendar/ImportEventsModal.tsx b/components/calendar/ImportEventsModal.tsx new file mode 100644 index 0000000..a686187 --- /dev/null +++ b/components/calendar/ImportEventsModal.tsx @@ -0,0 +1,81 @@ +import { useSignal } from '@preact/signals'; +import { useEffect } from 'preact/hooks'; + +import { Calendar } from '/lib/types.ts'; + +interface ImportEventsModalProps { + isOpen: boolean; + calendars: Pick[]; + onClickImport: (calendarId: string) => void; + onClose: () => void; +} + +export default function ImportEventsModal( + { isOpen, calendars, onClickImport, onClose }: ImportEventsModalProps, +) { + const newCalendarId = useSignal(null); + + useEffect(() => { + if (!isOpen) { + newCalendarId.value = null; + } else { + newCalendarId.value = calendars[0]!.id; + } + }, [isOpen]); + + return ( + <> +
+
+ +
+

Import Events

+
+
+ +
+ + calendar.id === newCalendarId.value)?.color + } rounded-full`} + title={calendars.find((calendar) => calendar.id === newCalendarId.value)?.color} + > + +
+
+
+
+ + +
+
+ + ); +} diff --git a/components/calendar/MainCalendar.tsx b/components/calendar/MainCalendar.tsx index df7855c..19a7370 100644 --- a/components/calendar/MainCalendar.tsx +++ b/components/calendar/MainCalendar.tsx @@ -1,20 +1,26 @@ import { useSignal } from '@preact/signals'; import { Calendar, CalendarEvent } from '/lib/types.ts'; -import { baseUrl, capitalizeWord, formatCalendarEventsToVCalendar } from '/lib/utils.ts'; +import { + baseUrl, + capitalizeWord, + formatCalendarEventsToVCalendar, + parseVCalendarFromTextContents, +} from '/lib/utils.ts'; import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/calendar/get-events.tsx'; import { RequestBody as AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/calendar/add-event.tsx'; import { RequestBody as DeleteRequestBody, ResponseBody as DeleteResponseBody, } from '/routes/api/calendar/delete-event.tsx'; -// import { RequestBody as ImportRequestBody, ResponseBody as ImportResponseBody } from '/routes/api/calendar/import-events.tsx'; +import { RequestBody as ImportRequestBody, ResponseBody as ImportResponseBody } from '/routes/api/calendar/import.tsx'; import CalendarViewDay from './CalendarViewDay.tsx'; import CalendarViewWeek from './CalendarViewWeek.tsx'; import CalendarViewMonth from './CalendarViewMonth.tsx'; import AddEventModal, { NewCalendarEvent } from './AddEventModal.tsx'; import ViewEventModal from './ViewEventModal.tsx'; import SearchEvents from './SearchEvents.tsx'; +import ImportEventsModal from './ImportEventsModal.tsx'; interface MainCalendarProps { initialCalendars: Pick[]; @@ -38,6 +44,9 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents, const newEventModal = useSignal<{ isOpen: boolean; initialStartDate?: Date; initiallyAllDay?: boolean }>({ isOpen: false, }); + const openImportModal = useSignal< + { isOpen: boolean } + >({ isOpen: false }); const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' }); const today = new Date().toISOString().substring(0, 10); @@ -247,6 +256,11 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents, } function onClickImportICS() { + openImportModal.value = { isOpen: true }; + isImportExportOptionsDropdownOpen.value = false; + } + + function onClickChooseImportCalendar(calendarId: string) { isImportExportOptionsDropdownOpen.value = false; if (isImporting.value) { @@ -266,7 +280,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents, } const reader = new FileReader(); - reader.onload = (fileRead) => { + reader.onload = async (fileRead) => { const importFileContents = fileRead.target?.result; if (!importFileContents || isImporting.value) { @@ -275,24 +289,33 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents, isImporting.value = true; - // try { - // const partialContacts = parseVCardFromTextContents(importFileContents!.toString()); + openImportModal.value = { isOpen: false }; - // const requestBody: ImportRequestBody = { partialContacts, page }; - // const response = await fetch(`/api/calendar/import`, { - // method: 'POST', - // body: JSON.stringify(requestBody), - // }); - // const result = await response.json() as ImportResponseBody; + try { + const partialCalendarEvents = parseVCalendarFromTextContents(importFileContents!.toString()); - // if (!result.success) { - // throw new Error('Failed to import contact!'); - // } + const requestBody: ImportRequestBody = { + partialCalendarEvents, + calendarIds: visibleCalendars.map((calendar) => calendar.id), + calendarView: view, + calendarStartDate: startDate, + calendarId, + }; - // contacts.value = [...result.contacts]; - // } catch (error) { - // console.error(error); - // } + const response = await fetch(`/api/calendar/import`, { + method: 'POST', + body: JSON.stringify(requestBody), + }); + const result = await response.json() as ImportResponseBody; + + if (!result.success) { + throw new Error('Failed to import file!'); + } + + calendarEvents.value = [...result.newCalendarEvents]; + } catch (error) { + console.error(error); + } isImporting.value = false; }; @@ -601,6 +624,15 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents, onClickDelete={onClickDeleteEvent} onClose={onCloseOpenEvent} /> + + { + openImportModal.value = { isOpen: false }; + }} + /> ); } diff --git a/fresh.gen.ts b/fresh.gen.ts index 009e9dc..77ec498 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -12,6 +12,7 @@ import * as $api_calendar_add from './routes/api/calendar/add.tsx'; import * as $api_calendar_delete_event from './routes/api/calendar/delete-event.tsx'; import * as $api_calendar_delete from './routes/api/calendar/delete.tsx'; import * as $api_calendar_get_events from './routes/api/calendar/get-events.tsx'; +import * as $api_calendar_import from './routes/api/calendar/import.tsx'; import * as $api_calendar_search_events from './routes/api/calendar/search-events.tsx'; import * as $api_calendar_update from './routes/api/calendar/update.tsx'; import * as $api_contacts_add from './routes/api/contacts/add.tsx'; @@ -72,6 +73,7 @@ const manifest = { './routes/api/calendar/delete-event.tsx': $api_calendar_delete_event, './routes/api/calendar/delete.tsx': $api_calendar_delete, './routes/api/calendar/get-events.tsx': $api_calendar_get_events, + './routes/api/calendar/import.tsx': $api_calendar_import, './routes/api/calendar/search-events.tsx': $api_calendar_search_events, './routes/api/calendar/update.tsx': $api_calendar_update, './routes/api/contacts/add.tsx': $api_contacts_add, diff --git a/lib/data/calendar.ts b/lib/data/calendar.ts index a35901a..25d48d3 100644 --- a/lib/data/calendar.ts +++ b/lib/data/calendar.ts @@ -189,7 +189,7 @@ export async function createCalendarEvent( const status: CalendarEvent['status'] = 'scheduled'; - const newCalendar = (await db.query( + const newCalendarEvent = (await db.query( sql`INSERT INTO "bewcloud_calendar_events" ( "user_id", "calendar_id", @@ -217,7 +217,7 @@ export async function createCalendarEvent( await updateCalendarRevision(calendar); - return newCalendar; + return newCalendarEvent; } export async function updateCalendarEvent(calendarEvent: CalendarEvent, oldCalendarId?: string) { diff --git a/lib/utils.ts b/lib/utils.ts index 6d0089d..780f75a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -657,7 +657,7 @@ export function formatCalendarEventsToVCalendar( ): string { const vCalendarText = calendarEvents.map((calendarEvent) => `BEGIN:VEVENT -DTSTAMP:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')} +DTSTAMP:${new Date(calendarEvent.created_at).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')} DTSTART:${new Date(calendarEvent.start_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')} DTEND:${new Date(calendarEvent.end_date).toISOString().substring(0, 19).replaceAll('-', '').replaceAll(':', '')} ORGANIZER;CN=:MAILTO:${calendarEvent.extra.organizer_email} @@ -698,7 +698,7 @@ export function parseVCalendarFromTextContents(text: string): Partial[]; + calendarId: string; +} + +export interface ResponseBody { + success: boolean; + newCalendarEvents: CalendarEvent[]; +} + +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 || !requestBody.calendarIds || !requestBody.partialCalendarEvents || + !requestBody.calendarView || !requestBody.calendarStartDate + ) { + return new Response('Bad Request', { status: 400 }); + } + + const calendar = await getCalendar(requestBody.calendarId, context.state.user.id); + + if (!calendar) { + return new Response('Not Found', { status: 404 }); + } + + if (requestBody.partialCalendarEvents.length === 0) { + return new Response('Not found', { status: 404 }); + } + + await concurrentPromises( + requestBody.partialCalendarEvents.map((partialCalendarEvent) => async () => { + if (partialCalendarEvent.title && partialCalendarEvent.start_date && partialCalendarEvent.end_date) { + const calendarEvent = await createCalendarEvent( + context.state.user!.id, + requestBody.calendarId, + partialCalendarEvent.title, + new Date(partialCalendarEvent.start_date), + new Date(partialCalendarEvent.end_date), + partialCalendarEvent.is_all_day, + ); + + const parsedExtra = JSON.stringify(partialCalendarEvent.extra || {}); + + if (parsedExtra !== '{}') { + calendarEvent.extra = partialCalendarEvent.extra!; + + await updateCalendarEvent(calendarEvent); + } + } + }), + 5, + ); + + const dateRange = { start: new Date(requestBody.calendarStartDate), end: new Date(requestBody.calendarStartDate) }; + + if (requestBody.calendarView === 'day') { + dateRange.start.setDate(dateRange.start.getDate() - 1); + dateRange.end.setDate(dateRange.end.getDate() + 1); + } else if (requestBody.calendarView === 'week') { + dateRange.start.setDate(dateRange.start.getDate() - 7); + dateRange.end.setDate(dateRange.end.getDate() + 7); + } else { + dateRange.start.setDate(dateRange.start.getDate() - 7); + dateRange.end.setDate(dateRange.end.getDate() + 31); + } + + const newCalendarEvents = await getCalendarEvents(context.state.user.id, requestBody.calendarIds, dateRange); + + const responseBody: ResponseBody = { success: true, newCalendarEvents }; + + return new Response(JSON.stringify(responseBody)); + }, +};