Files CRUD.
Remove Contacts and Calendar + CardDav and CalDav.
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 ? <> </> : 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 ? <> </> : null}
|
||||
</span>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user