Files CRUD.

Remove Contacts and Calendar + CardDav and CalDav.
This commit is contained in:
Bruno Bernardino
2024-04-03 14:02:04 +01:00
parent c4788761d2
commit 4e5fdd569a
89 changed files with 2302 additions and 8001 deletions

View File

@@ -1,23 +0,0 @@
import { Calendar, CalendarEvent } from '/lib/types.ts';
import MainCalendar from '/components/calendar/MainCalendar.tsx';
interface CalendarWrapperProps {
initialCalendars: Pick<Calendar, 'id' | 'name' | 'color' | 'is_visible' | 'extra'>[];
initialCalendarEvents: CalendarEvent[];
view: 'day' | 'week' | 'month';
startDate: string;
}
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
export default function CalendarWrapper(
{ initialCalendars, initialCalendarEvents, view, startDate }: CalendarWrapperProps,
) {
return (
<MainCalendar
initialCalendars={initialCalendars}
initialCalendarEvents={initialCalendarEvents}
view={view}
startDate={startDate}
/>
);
}

View File

@@ -1,299 +0,0 @@
import { useSignal } from '@preact/signals';
import { Calendar } from '/lib/types.ts';
import { CALENDAR_COLOR_OPTIONS } from '/lib/utils/calendar.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<boolean>(false);
const isDeleting = useSignal<boolean>(false);
const isSaving = useSignal<boolean>(false);
const calendars = useSignal<Calendar[]>(initialCalendars);
const openCalendar = useSignal<Calendar | null>(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 (
<>
<section class='flex flex-row items-center justify-between mb-4'>
<a href='/calendar' class='mr-2'>View calendar</a>
<section class='flex items-center'>
<button
class='inline-block justify-center gap-x-1.5 rounded-md bg-[#51A4FB] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-400 ml-2'
type='button'
title='Add new calendar'
onClick={() => onClickAddCalendar()}
>
<img
src='/images/add.svg'
alt='Add new calendar'
class={`white ${isAdding.value ? 'animate-spin' : ''}`}
width={20}
height={20}
/>
</button>
</section>
</section>
<section class='mx-auto max-w-7xl my-8'>
<table class='w-full border-collapse bg-gray-900 text-left text-sm text-white shadow-sm rounded-md'>
<thead>
<tr>
<th scope='col' class='px-6 py-4 font-medium'>Name</th>
<th scope='col' class='px-6 py-4 font-medium'>Color</th>
<th scope='col' class='px-6 py-4 font-medium'>Visible?</th>
<th scope='col' class='px-6 py-4 font-medium w-20'></th>
</tr>
</thead>
<tbody class='divide-y divide-slate-600 border-t border-slate-600'>
{calendars.value.map((calendar) => (
<tr class='bg-slate-700 hover:bg-slate-600 group'>
<td class='flex gap-3 px-6 py-4 font-medium'>
{calendar.name}
</td>
<td class='px-6 py-4 text-slate-200'>
<span
class={`w-5 h-5 inline-block ${calendar.color} rounded-full cursor-pointer`}
title={calendar.color}
onClick={() => openCalendar.value = { ...calendar }}
>
</span>
</td>
<td class='px-6 py-4'>
{calendar.is_visible ? 'Yes' : 'No'}
</td>
<td class='px-6 py-4'>
<span
class='invisible cursor-pointer group-hover:visible opacity-50 hover:opacity-100'
onClick={() => onClickDeleteCalendar(calendar.id)}
>
<img
src='/images/delete.svg'
class='red drop-shadow-md'
width={24}
height={24}
alt='Delete calendar'
title='Delete calendar'
/>
</span>
</td>
</tr>
))}
{calendars.value.length === 0
? (
<tr>
<td class='flex gap-3 px-6 py-4 font-normal' colspan={4}>
<div class='text-md'>
<div class='font-medium text-slate-400'>No calendars to show</div>
</div>
</td>
</tr>
)
: null}
</tbody>
</table>
<span
class={`flex justify-end items-center text-sm mt-1 mx-2 text-slate-100`}
>
{isDeleting.value
? (
<>
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Deleting...
</>
)
: null}
{isSaving.value
? (
<>
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Saving...
</>
)
: null}
{!isDeleting.value && !isSaving.value ? <>&nbsp;</> : null}
</span>
</section>
<section
class={`fixed ${
openCalendar.value ? 'block' : 'hidden'
} z-40 w-screen h-screen inset-0 bg-gray-900 bg-opacity-60`}
>
</section>
<section
class={`fixed ${
openCalendar.value ? 'block' : 'hidden'
} z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 bg-slate-600 text-white rounded-md px-8 py-6 drop-shadow-lg`}
>
<h1 class='text-2xl font-semibold my-5'>Edit Calendar</h1>
<section class='py-5 my-2 border-y border-slate-500'>
<fieldset class='block mb-2'>
<label class='text-slate-300 block pb-1' for='calendar_name'>Name</label>
<input
class='input-field'
type='text'
name='calendar_name'
id='calendar_name'
value={openCalendar.value?.name || ''}
onInput={(event) => openCalendar.value = { ...openCalendar.value!, name: event.currentTarget.value }}
placeholder='Personal'
/>
</fieldset>
<fieldset class='block mb-2'>
<label class='text-slate-300 block pb-1' for='calendar_color'>Color</label>
<section class='flex items-center justify-between'>
<select
class='input-field mr-2 !w-5/6'
name='calendar_color'
id='calendar_color'
value={openCalendar.value?.color || ''}
onChange={(event) => openCalendar.value = { ...openCalendar.value!, color: event.currentTarget.value }}
>
{CALENDAR_COLOR_OPTIONS.map((color) => <option>{color}</option>)}
</select>
<span class={`w-5 h-5 block ${openCalendar.value?.color} rounded-full`} title={openCalendar.value?.color}>
</span>
</section>
</fieldset>
<fieldset class='block mb-2'>
<label class='text-slate-300 block pb-1' for='calendar_is_visible'>Visible?</label>
<input
type='checkbox'
name='calendar_is_visible'
id='calendar_is_visible'
value='true'
checked={openCalendar.value?.is_visible}
onChange={(event) =>
openCalendar.value = { ...openCalendar.value!, is_visible: event.currentTarget.checked }}
/>
</fieldset>
</section>
<footer class='flex justify-between'>
<button
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
onClick={() => onClickSaveOpenCalendar()}
>
Save
</button>
<button
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
onClick={() => openCalendar.value = null}
>
Close
</button>
</footer>
</section>
</>
);
}

View File

@@ -1,219 +0,0 @@
import { useSignal } from '@preact/signals';
import { Calendar, CalendarEvent } from '/lib/types.ts';
import { capitalizeWord, convertObjectToFormData } from '/lib/utils/misc.ts';
import { FormField, generateFieldHtml } from '/lib/form-utils.tsx';
import {
RequestBody as DeleteRequestBody,
ResponseBody as DeleteResponseBody,
} from '/routes/api/calendar/delete-event.tsx';
interface ViewCalendarEventProps {
initialCalendarEvent: CalendarEvent;
calendars: Calendar[];
formData: Record<string, any>;
error?: string;
notice?: string;
}
export function formFields(calendarEvent: CalendarEvent, calendars: Calendar[]) {
const fields: FormField[] = [
{
name: 'title',
label: 'Title',
type: 'text',
placeholder: 'Dentis',
value: calendarEvent.title,
required: true,
},
{
name: 'calendar_id',
label: 'Calendar',
type: 'select',
value: calendarEvent.calendar_id,
options: calendars.map((calendar) => ({ label: calendar.name, value: calendar.id })),
required: true,
},
{
name: 'start_date',
label: 'Start date',
type: 'datetime-local',
value: new Date(calendarEvent.start_date).toISOString().substring(0, 16),
required: true,
},
{
name: 'end_date',
label: 'End date',
type: 'datetime-local',
value: new Date(calendarEvent.end_date).toISOString().substring(0, 16),
required: true,
},
{
name: 'is_all_day',
label: 'All-day?',
type: 'checkbox',
placeholder: 'YYYYMMDD',
value: 'true',
required: false,
checked: calendarEvent.is_all_day,
},
{
name: 'status',
label: 'Status',
type: 'select',
value: calendarEvent.status,
options: (['scheduled', 'pending', 'canceled'] as CalendarEvent['status'][]).map((status) => ({
label: capitalizeWord(status),
value: status,
})),
required: true,
},
{
name: 'description',
label: 'Description',
type: 'textarea',
placeholder: 'Just a regular check-up.',
value: calendarEvent.extra.description,
required: false,
},
{
name: 'url',
label: 'URL',
type: 'url',
placeholder: 'https://example.com',
value: calendarEvent.extra.url,
required: false,
},
{
name: 'location',
label: 'Location',
type: 'text',
placeholder: 'Birmingham, UK',
value: calendarEvent.extra.location,
required: false,
},
{
name: 'transparency',
label: 'Transparency',
type: 'select',
value: calendarEvent.extra.transparency,
options: (['default', 'opaque', 'transparent'] as CalendarEvent['extra']['transparency'][]).map((
transparency,
) => ({
label: capitalizeWord(transparency),
value: transparency,
})),
required: true,
},
];
return fields;
}
export default function viewCalendarEvent(
{ initialCalendarEvent, calendars, formData: formDataObject, error, notice }: ViewCalendarEventProps,
) {
const isDeleting = useSignal<boolean>(false);
const calendarEvent = useSignal<CalendarEvent>(initialCalendarEvent);
const formData = convertObjectToFormData(formDataObject);
async function onClickDeleteEvent() {
if (confirm('Are you sure you want to delete this event?')) {
if (isDeleting.value) {
return;
}
isDeleting.value = true;
try {
const requestBody: DeleteRequestBody = {
calendarIds: calendars.map((calendar) => calendar.id),
calendarView: 'day',
calendarStartDate: new Date().toISOString().substring(0, 10),
calendarEventId: calendarEvent.value.id,
calendarId: calendarEvent.value.calendar_id,
};
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!');
}
window.location.href = '/calendar';
} catch (error) {
console.error(error);
}
isDeleting.value = false;
}
}
return (
<>
<section class='flex flex-row items-center justify-between mb-4'>
<a href='/calendar' class='mr-2'>View calendar</a>
<section class='flex items-center'>
<button
class='inline-block justify-center gap-x-1.5 rounded-md bg-red-800 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 ml-2'
type='button'
title='Delete event'
onClick={() => onClickDeleteEvent()}
>
<img
src='/images/delete.svg'
alt='Delete event'
class={`white ${isDeleting.value ? 'animate-spin' : ''}`}
width={20}
height={20}
/>
</button>
</section>
</section>
<section class='mx-auto max-w-7xl my-8'>
{error
? (
<section class='notification-error'>
<h3>Failed to update!</h3>
<p>{error}</p>
</section>
)
: null}
{notice
? (
<section class='notification-success'>
<h3>Success!</h3>
<p>{notice}</p>
</section>
)
: null}
<form method='POST' class='mb-12'>
{formFields(calendarEvent.peek(), calendars).map((field) => generateFieldHtml(field, formData))}
<section class='flex justify-end mt-8 mb-4'>
<button class='button' type='submit'>Update event</button>
</section>
</form>
<span
class={`flex justify-end items-center text-sm mt-1 mx-2 text-slate-100`}
>
{isDeleting.value
? (
<>
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Deleting...
</>
)
: null}
{!isDeleting.value ? <>&nbsp;</> : null}
</span>
</section>
</>
);
}