import { useSignal } from '@preact/signals'; import { Calendar, CalendarEvent } from '/lib/models/calendar.ts'; import { capitalizeWord } from '/lib/utils/misc.ts'; import { generateVCalendar } from '/lib/utils/calendar.ts'; import { RequestBody as ExportRequestBody, ResponseBody as ExportResponseBody, } from '/routes/api/calendar/export-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.tsx'; import CalendarViewDay from './CalendarViewDay.tsx'; import CalendarViewWeek from './CalendarViewWeek.tsx'; import CalendarViewMonth from './CalendarViewMonth.tsx'; import AddEventModal from './AddEventModal.tsx'; import ViewEventModal from './ViewEventModal.tsx'; import SearchEvents from './SearchEvents.tsx'; import ImportEventsModal from './ImportEventsModal.tsx'; interface MainCalendarProps { initialCalendars: Calendar[]; initialCalendarEvents: CalendarEvent[]; view: 'day' | 'week' | 'month'; startDate: string; baseUrl: string; timezoneId: string; timezoneUtcOffset: number; } export default function MainCalendar( { initialCalendars, initialCalendarEvents, view, startDate, baseUrl, timezoneId, timezoneUtcOffset }: MainCalendarProps, ) { const isAdding = useSignal(false); const isDeleting = useSignal(false); const isExporting = useSignal(false); const isImporting = useSignal(false); const calendars = useSignal(initialCalendars); const isViewOptionsDropdownOpen = useSignal(false); const isImportExportOptionsDropdownOpen = useSignal(false); const calendarEvents = useSignal(initialCalendarEvents); const openEventModal = useSignal< { isOpen: boolean; calendar?: typeof initialCalendars[number]; calendarEvent?: CalendarEvent } >({ isOpen: false }); 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', timeZone: timezoneId, // Calendar dates are parsed without timezone info, so we need to force to a specific one so it's consistent across db, server, and client }); const today = new Date().toISOString().substring(0, 10); const visibleCalendars = calendars.value.filter((calendar) => calendar.isVisible); function onClickAddEvent(startDate = new Date(), isAllDay = false) { if (newEventModal.value.isOpen) { newEventModal.value = { isOpen: false, }; return; } if (calendars.value.length === 0) { alert('You need to create a calendar first!'); return; } newEventModal.value = { isOpen: true, initialStartDate: startDate, initiallyAllDay: isAllDay, }; } async function onClickSaveNewEvent(newEvent: CalendarEvent) { if (isAdding.value) { return; } if (!newEvent) { return; } isAdding.value = true; try { const requestBody: AddRequestBody = { calendarIds: visibleCalendars.map((calendar) => calendar.uid!), calendarView: view, calendarStartDate: startDate, calendarId: newEvent.calendarId, title: newEvent.title, startDate: new Date(newEvent.startDate).toISOString(), endDate: new Date(newEvent.endDate).toISOString(), isAllDay: newEvent.isAllDay, }; const response = await fetch(`/api/calendar/add-event`, { method: 'POST', body: JSON.stringify(requestBody), }); const result = await response.json() as AddResponseBody; if (!result.success) { throw new Error('Failed to add event!'); } calendarEvents.value = [...result.newCalendarEvents]; newEventModal.value = { isOpen: false, }; } catch (error) { console.error(error); } isAdding.value = false; } function onCloseNewEvent() { newEventModal.value = { isOpen: false, }; } function toggleImportExportOptionsDropdown() { isImportExportOptionsDropdownOpen.value = !isImportExportOptionsDropdownOpen.value; } function toggleViewOptionsDropdown() { isViewOptionsDropdownOpen.value = !isViewOptionsDropdownOpen.value; } function onClickOpenEvent(calendarEvent: CalendarEvent) { if (openEventModal.value.isOpen) { openEventModal.value = { isOpen: false, }; return; } const calendar = calendars.value.find((calendar) => calendar.uid === calendarEvent.calendarId)!; openEventModal.value = { isOpen: true, calendar, calendarEvent, }; } async function onClickDeleteEvent(calendarEventId: string) { if (confirm('Are you sure you want to delete this event?')) { if (isDeleting.value) { return; } isDeleting.value = true; try { const requestBody: DeleteRequestBody = { calendarIds: visibleCalendars.map((calendar) => calendar.uid!), calendarView: view, calendarStartDate: startDate, calendarEventId, calendarId: calendarEvents.value.find((calendarEvent) => calendarEvent.uid === calendarEventId)!.calendarId, }; const response = await fetch(`/api/calendar/delete-event`, { method: 'POST', body: JSON.stringify(requestBody), }); const result = await response.json() as DeleteResponseBody; if (!result.success) { throw new Error('Failed to delete event!'); } calendarEvents.value = [...result.newCalendarEvents]; } catch (error) { console.error(error); } isDeleting.value = false; openEventModal.value = { isOpen: false }; } } function onCloseOpenEvent() { openEventModal.value = { isOpen: false, }; } function onClickChangeStartDate(changeTo: 'previous' | 'next' | 'today') { const previousDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() - 1)).toISOString() .substring(0, 10); const nextDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() + 1)).toISOString() .substring(0, 10); const previousWeek = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() - 7)).toISOString() .substring(0, 10); const nextWeek = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() + 7)).toISOString() .substring(0, 10); const previousMonth = new Date(new Date(startDate).setUTCMonth(new Date(startDate).getUTCMonth() - 1)).toISOString() .substring(0, 10); const nextMonth = new Date(new Date(startDate).setUTCMonth(new Date(startDate).getUTCMonth() + 1)).toISOString() .substring(0, 10); if (changeTo === 'today') { if (today === startDate) { return; } window.location.href = `/calendar?view=${view}&startDate=${today}`; return; } if (changeTo === 'previous') { let newStartDate = previousMonth; if (view === 'day') { newStartDate = previousDay; } else if (view === 'week') { newStartDate = previousWeek; } if (newStartDate === startDate) { return; } window.location.href = `/calendar?view=${view}&startDate=${newStartDate}`; return; } let newStartDate = nextMonth; if (view === 'day') { newStartDate = nextDay; } else if (view === 'week') { newStartDate = nextWeek; } if (newStartDate === startDate) { return; } window.location.href = `/calendar?view=${view}&startDate=${newStartDate}`; } function onClickChangeView(newView: MainCalendarProps['view']) { if (view === newView) { isViewOptionsDropdownOpen.value = false; return; } window.location.href = `/calendar?view=${newView}&startDate=${startDate}`; } function onClickImportICS() { openImportModal.value = { isOpen: true }; isImportExportOptionsDropdownOpen.value = false; } function onClickChooseImportCalendar(calendarId: string) { isImportExportOptionsDropdownOpen.value = false; if (isImporting.value) { return; } const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.click(); fileInput.onchange = (event) => { const files = (event.target as HTMLInputElement)?.files!; const file = files[0]; if (!file) { return; } const reader = new FileReader(); reader.onload = async (fileRead) => { const importFileContents = fileRead.target?.result; if (!importFileContents || isImporting.value) { return; } isImporting.value = true; openImportModal.value = { isOpen: false }; try { const icsToImport = importFileContents!.toString(); const requestBody: ImportRequestBody = { icsToImport, calendarIds: visibleCalendars.map((calendar) => calendar.uid!), calendarView: view, calendarStartDate: startDate, calendarId, }; 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; }; reader.readAsText(file, 'UTF-8'); }; } async function onClickExportICS() { isImportExportOptionsDropdownOpen.value = false; if (isExporting.value) { return; } isExporting.value = true; const fileName = ['calendar-', new Date().toISOString().substring(0, 19).replace(/:/g, '-'), '.ics'] .join(''); try { const requestBody: ExportRequestBody = { calendarIds: visibleCalendars.map((calendar) => calendar.uid!) }; const response = await fetch(`/api/calendar/export-events`, { method: 'POST', body: JSON.stringify(requestBody), }); const result = await response.json() as ExportResponseBody; if (!result.success) { throw new Error('Failed to get contact!'); } const exportContents = generateVCalendar([...result.calendarEvents]); // Add content-type const vCardContent = ['data:text/calendar; charset=utf-8,', encodeURIComponent(exportContents)].join(''); // Download the file const data = vCardContent; const link = document.createElement('a'); link.setAttribute('href', data); link.setAttribute('download', fileName); link.click(); link.remove(); } catch (error) { console.error(error); } isExporting.value = false; } return ( <>
Manage calendars

{view === 'day' ? ( ) : null} {view === 'week' ? ( ) : null} {view === 'month' ? ( ) : null} {isDeleting.value ? ( <> Deleting... ) : null} {isExporting.value ? ( <> Exporting... ) : null} {isImporting.value ? ( <> Importing... ) : null} {!isDeleting.value && !isExporting.value && !isImporting.value ? <>  : null}
CalDav URL:{' '} {baseUrl}/caldav
{ openImportModal.value = { isOpen: false }; }} /> ); }