Open event details in a modal
This commit is contained in:
@@ -26,9 +26,19 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
const isImportExportOptionsDropdownOpen = useSignal<boolean>(false);
|
const isImportExportOptionsDropdownOpen = useSignal<boolean>(false);
|
||||||
const calendarEvents = useSignal<CalendarEvent[]>(initialCalendarEvents);
|
const calendarEvents = useSignal<CalendarEvent[]>(initialCalendarEvents);
|
||||||
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
||||||
|
const openEvent = useSignal<CalendarEvent | null>(null);
|
||||||
|
|
||||||
const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
|
const dateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long' });
|
||||||
const hourFormat = new Intl.DateTimeFormat('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
const hourFormat = new Intl.DateTimeFormat('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' });
|
||||||
|
const eventDateFormat = new Intl.DateTimeFormat('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
const allDayEventDateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
const today = new Date().toISOString().substring(0, 10);
|
const today = new Date().toISOString().substring(0, 10);
|
||||||
|
|
||||||
function onClickAddEvent() {
|
function onClickAddEvent() {
|
||||||
@@ -111,6 +121,8 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
isDeleting.value = false;
|
isDeleting.value = false;
|
||||||
|
|
||||||
|
openEvent.value = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,127 +513,147 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class='mx-auto max-w-7xl my-8'>
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
<section>
|
{view === 'day'
|
||||||
<section class='shadow-md lg:flex lg:flex-auto lg:flex-col rounded-md'>
|
? (
|
||||||
<section class='grid grid-cols-7 gap-px border-b border-slate-500 bg-slate-700 text-center text-xs font-semibold text-white lg:flex-none rounded-t-md'>
|
<section>
|
||||||
<div class='flex justify-center bg-gray-900 py-2 rounded-tl-md'>
|
TODO: Build day view
|
||||||
<span>M</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>on</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
|
||||||
<span>T</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>ue</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
|
||||||
<span>W</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>ed</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
|
||||||
<span>T</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>hu</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
|
||||||
<span>F</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>ri</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
|
||||||
<span>S</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>at</span>
|
|
||||||
</div>
|
|
||||||
<div class='flex justify-center bg-gray-900 py-2 rounded-tr-md'>
|
|
||||||
<span>S</span>
|
|
||||||
<span class='sr-only sm:not-sr-only'>un</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<section class='flex bg-slate-500 text-xs leading-6 text-white lg:flex-auto rounded-b-md'>
|
)
|
||||||
<section class='w-full grid lg:grid-cols-7 lg:grid-rows-5 lg:gap-px rounded-b-md'>
|
: null}
|
||||||
{weeks.map((week, weekIndex) =>
|
{view === 'week'
|
||||||
week.map((day, dayIndex) => {
|
? (
|
||||||
const shortIsoDate = day.date.toISOString().substring(0, 10);
|
<section>
|
||||||
|
TODO: Build week view
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
{view === 'month'
|
||||||
|
? (
|
||||||
|
<section>
|
||||||
|
<section class='shadow-md lg:flex lg:flex-auto lg:flex-col rounded-md'>
|
||||||
|
<section class='grid grid-cols-7 gap-px border-b border-slate-500 bg-slate-700 text-center text-xs font-semibold text-white lg:flex-none rounded-t-md'>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2 rounded-tl-md'>
|
||||||
|
<span>M</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>on</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
|
<span>T</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>ue</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
|
<span>W</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>ed</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
|
<span>T</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>hu</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
|
<span>F</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>ri</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
|
<span>S</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>at</span>
|
||||||
|
</div>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2 rounded-tr-md'>
|
||||||
|
<span>S</span>
|
||||||
|
<span class='sr-only sm:not-sr-only'>un</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class='flex bg-slate-500 text-xs leading-6 text-white lg:flex-auto rounded-b-md'>
|
||||||
|
<section class='w-full grid lg:grid-cols-7 lg:grid-rows-5 lg:gap-px rounded-b-md'>
|
||||||
|
{weeks.map((week, weekIndex) =>
|
||||||
|
week.map((day, dayIndex) => {
|
||||||
|
const shortIsoDate = day.date.toISOString().substring(0, 10);
|
||||||
|
|
||||||
const startDayDate = new Date(shortIsoDate);
|
const startDayDate = new Date(shortIsoDate);
|
||||||
const endDayDate = new Date(shortIsoDate);
|
const endDayDate = new Date(shortIsoDate);
|
||||||
endDayDate.setHours(23);
|
endDayDate.setHours(23);
|
||||||
endDayDate.setMinutes(59);
|
endDayDate.setMinutes(59);
|
||||||
endDayDate.setSeconds(59);
|
endDayDate.setSeconds(59);
|
||||||
endDayDate.setMilliseconds(999);
|
endDayDate.setMilliseconds(999);
|
||||||
|
|
||||||
const isBottomLeftDay = weekIndex === weeks.length - 1 && dayIndex === 0;
|
const isBottomLeftDay = weekIndex === weeks.length - 1 && dayIndex === 0;
|
||||||
const isBottomRightDay = weekIndex === weeks.length - 1 && dayIndex === week.length - 1;
|
const isBottomRightDay = weekIndex === weeks.length - 1 && dayIndex === week.length - 1;
|
||||||
|
|
||||||
const isToday = today === shortIsoDate;
|
const isToday = today === shortIsoDate;
|
||||||
|
|
||||||
// TODO: Consider events that span multiple days
|
// TODO: Consider events that span multiple days
|
||||||
const dayEvents = calendarEvents.value.filter((calendarEvent) =>
|
const dayEvents = calendarEvents.value.filter((calendarEvent) =>
|
||||||
new Date(calendarEvent.start_date) >= startDayDate &&
|
new Date(calendarEvent.start_date) >= startDayDate &&
|
||||||
new Date(calendarEvent.end_date) <= endDayDate
|
new Date(calendarEvent.end_date) <= endDayDate
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
class={`relative ${day.isSameMonth ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
class={`relative ${day.isSameMonth ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
||||||
day.isSameMonth ? '' : 'text-slate-100'
|
day.isSameMonth ? '' : 'text-slate-100'
|
||||||
} ${isBottomLeftDay ? 'rounded-bl-md' : ''} ${isBottomRightDay ? 'rounded-br-md' : ''}`}
|
} ${isBottomLeftDay ? 'rounded-bl-md' : ''} ${isBottomRightDay ? 'rounded-br-md' : ''}`}
|
||||||
>
|
>
|
||||||
<time
|
<time
|
||||||
datetime={shortIsoDate}
|
datetime={shortIsoDate}
|
||||||
class={`${
|
class={`${
|
||||||
isToday
|
isToday
|
||||||
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{day.date.getDate()}
|
{day.date.getDate()}
|
||||||
</time>
|
</time>
|
||||||
{dayEvents.length > 0
|
{dayEvents.length > 0
|
||||||
? (
|
? (
|
||||||
<ol class='mt-2'>
|
<ol class='mt-2'>
|
||||||
{[...dayEvents].slice(0, 2).map((dayEvent) => (
|
{[...dayEvents].slice(0, 2).map((dayEvent) => (
|
||||||
<li class='mb-1'>
|
<li class='mb-1'>
|
||||||
<a
|
<a
|
||||||
href='#'
|
href='javascript:void(0);'
|
||||||
class={`flex px-2 py-0 rounded-md hover:no-underline hover:opacity-60 ${
|
class={`flex px-2 py-0 rounded-md hover:no-underline hover:opacity-60 ${
|
||||||
visibleCalendars.find((calendar) => calendar.id === dayEvent.calendar_id)
|
visibleCalendars.find((calendar) => calendar.id === dayEvent.calendar_id)
|
||||||
?.color || 'bg-gray-700'
|
?.color || 'bg-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
onClick={() => openEvent.value = dayEvent}
|
||||||
<time
|
>
|
||||||
datetime={new Date(dayEvent.start_date).toISOString()}
|
<time
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
datetime={new Date(dayEvent.start_date).toISOString()}
|
||||||
>
|
class='mr-2 flex-none text-slate-100 block'
|
||||||
{hourFormat.format(new Date(dayEvent.start_date))}
|
>
|
||||||
</time>
|
{hourFormat.format(new Date(dayEvent.start_date))}
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
</time>
|
||||||
{dayEvent.title}
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
</p>
|
{dayEvent.title}
|
||||||
</a>
|
</p>
|
||||||
</li>
|
</a>
|
||||||
))}
|
</li>
|
||||||
{dayEvents.length > 2
|
))}
|
||||||
? (
|
{dayEvents.length > 2
|
||||||
<li class='mb-1'>
|
? (
|
||||||
<a
|
<li class='mb-1'>
|
||||||
href='#'
|
<a
|
||||||
class='flex bg-purple-600 px-2 py-0 rounded-md hover:no-underline hover:bg-purple-500'
|
href={`/calendar/view=day&startDate=${shortIsoDate}`}
|
||||||
>
|
class='flex bg-gray-700 px-2 py-0 rounded-md hover:no-underline hover:opacity-60'
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
target='_blank'
|
||||||
...{dayEvents.length - 2} more event{dayEvents.length - 2 === 1 ? '' : 's'}
|
>
|
||||||
</p>
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
</a>
|
...{dayEvents.length - 2} more event{dayEvents.length - 2 === 1 ? '' : 's'}
|
||||||
</li>
|
</p>
|
||||||
)
|
</a>
|
||||||
: null}
|
</li>
|
||||||
</ol>
|
)
|
||||||
)
|
: null}
|
||||||
: null}
|
</ol>
|
||||||
</section>
|
)
|
||||||
);
|
: null}
|
||||||
})
|
</section>
|
||||||
)}
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
)
|
||||||
</section>
|
: null}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class={`flex justify-end items-center text-sm mt-1 mx-2 text-slate-100`}
|
class={`flex justify-end items-center text-sm mt-1 mx-2 text-slate-100`}
|
||||||
@@ -656,6 +688,60 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/dav/principals/</code>{' '}
|
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/dav/principals/</code>{' '}
|
||||||
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/dav/calendars/</code>
|
<code class='bg-slate-600 mx-2 px-2 py-1 rounded-md'>{baseUrl}/dav/calendars/</code>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class={`fixed ${openEvent.value ? 'block' : 'hidden'} z-40 w-screen h-screen inset-0 bg-gray-900 bg-opacity-60`}
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class={`fixed ${
|
||||||
|
openEvent.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 space-y-5 drop-shadow-lg`}
|
||||||
|
>
|
||||||
|
<h1 class='text-2xl font-semibold'>{openEvent.value?.title || ''}</h1>
|
||||||
|
<header class='py-5 border-t border-b border-slate-500 font-semibold flex justify-between'>
|
||||||
|
<span>
|
||||||
|
{openEvent.value?.start_date ? allDayEventDateFormat.format(new Date(openEvent.value.start_date)) : ''}
|
||||||
|
</span>
|
||||||
|
{openEvent.value?.is_all_day ? <span>All-day</span> : (
|
||||||
|
<span>
|
||||||
|
{openEvent.value?.start_date ? hourFormat.format(new Date(openEvent.value.start_date)) : ''} -{' '}
|
||||||
|
{openEvent.value?.end_date ? hourFormat.format(new Date(openEvent.value.end_date)) : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
{openEvent.value?.extra.description
|
||||||
|
? (
|
||||||
|
<section class='py-5 border-b border-slate-500'>
|
||||||
|
<p>{openEvent.value.extra.description}</p>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
<section class='py-5 border-b border-slate-500'>
|
||||||
|
<p>TODO: location, calendar, recurrence, reminders</p>
|
||||||
|
</section>
|
||||||
|
<footer class='flex justify-between'>
|
||||||
|
<button
|
||||||
|
class='px-5 py-2 bg-slate-600 hover:bg-red-600 text-white cursor-pointer rounded-md'
|
||||||
|
onClick={() => onClickDeleteEvent(openEvent.value?.id || '')}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={`/calendar/events/${openEvent.value?.id}`}
|
||||||
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
class='px-5 py-2 bg-slate-600 hover:bg-slate-500 text-white cursor-pointer rounded-md'
|
||||||
|
onClick={() => openEvent.value = null}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
10
lib/types.ts
10
lib/types.ts
@@ -147,7 +147,6 @@ export interface Calendar {
|
|||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: I don't really invite people to events in my private calendars, so I don't think I'll need that complexity
|
|
||||||
// TODO: Finish (more fields)
|
// TODO: Finish (more fields)
|
||||||
export interface CalendarEvent {
|
export interface CalendarEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -162,8 +161,17 @@ export interface CalendarEvent {
|
|||||||
extra: {
|
extra: {
|
||||||
description?: string;
|
description?: string;
|
||||||
location?: string;
|
location?: string;
|
||||||
|
attendees?: CalendarEventAttendee[];
|
||||||
visibility: 'default' | 'public' | 'private';
|
visibility: 'default' | 'public' | 'private';
|
||||||
|
is_recurring?: boolean;
|
||||||
|
recurring_rrule?: string;
|
||||||
};
|
};
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CalendarEventAttendee {
|
||||||
|
email: string;
|
||||||
|
status: 'accepted' | 'rejected' | 'invited';
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ async function getCalendarEvents(userId: string, calendarIds: string[]): Promise
|
|||||||
{
|
{
|
||||||
id: 'event-2',
|
id: 'event-2',
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
calendar_id: 'personal-1',
|
calendar_id: 'family-1',
|
||||||
revision: 'fake-rev',
|
revision: 'fake-rev',
|
||||||
title: 'Dermatologist',
|
title: 'Dermatologist',
|
||||||
start_date: new Date('2024-03-17T16:30:00.000Z'),
|
start_date: new Date('2024-03-17T16:30:00.000Z'),
|
||||||
@@ -83,6 +83,22 @@ async function getCalendarEvents(userId: string, calendarIds: string[]): Promise
|
|||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
created_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('2024-03-18T09:00:00.000Z'),
|
||||||
|
end_date: new Date('2024-03-18T21:00:00.000Z'),
|
||||||
|
is_all_day: true,
|
||||||
|
status: 'scheduled',
|
||||||
|
extra: {
|
||||||
|
visibility: 'default',
|
||||||
|
},
|
||||||
|
updated_at: new Date(),
|
||||||
|
created_at: new Date(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +117,7 @@ export const handler: Handlers<Data, FreshContextState> = {
|
|||||||
|
|
||||||
const searchParams = new URL(request.url).searchParams;
|
const searchParams = new URL(request.url).searchParams;
|
||||||
|
|
||||||
const view = (searchParams.get('view') as Data['view']) || 'week';
|
const view = (searchParams.get('view') as Data['view']) || 'month';
|
||||||
const startDate = searchParams.get('startDate') || new Date().toISOString().substring(0, 10);
|
const startDate = searchParams.get('startDate') || new Date().toISOString().substring(0, 10);
|
||||||
|
|
||||||
const userCalendars = await getCalendars(context.state.user.id);
|
const userCalendars = await getCalendars(context.state.user.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user