Add UI for day view
This commit is contained in:
@@ -30,13 +30,11 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
|
|
||||||
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', {
|
const dayFormat = new Intl.DateTimeFormat('en-GB', {
|
||||||
year: 'numeric',
|
weekday: 'long',
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour12: false,
|
month: 'long',
|
||||||
hour: '2-digit',
|
year: 'numeric',
|
||||||
minute: '2-digit',
|
|
||||||
});
|
});
|
||||||
const allDayEventDateFormat = new Intl.DateTimeFormat('en-GB', { year: 'numeric', month: 'long', day: 'numeric' });
|
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);
|
||||||
@@ -331,7 +329,26 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
const visibleCalendarEvents = calendarEvents.value;
|
const visibleCalendarEvents = calendarEvents.value;
|
||||||
|
|
||||||
// TODO: Send in / consider user timezone
|
// TODO: Send in / consider user timezone
|
||||||
const weeks = getWeeksForMonth(new Date(startDate));
|
const weeks = view === 'month' ? getWeeksForMonth(new Date(startDate)) : [];
|
||||||
|
const hours: { date: Date; isCurrentHour: boolean }[] = view === 'day'
|
||||||
|
? Array.from({ length: 24 }).map((_, index) => {
|
||||||
|
const hourNumber = index;
|
||||||
|
|
||||||
|
const date = new Date(startDate);
|
||||||
|
date.setHours(hourNumber);
|
||||||
|
|
||||||
|
const shortIsoDate = date.toISOString().substring(0, 10);
|
||||||
|
|
||||||
|
const isCurrentHour = shortIsoDate === today && new Date().getHours() === hourNumber;
|
||||||
|
|
||||||
|
return {
|
||||||
|
date,
|
||||||
|
isCurrentHour,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// TODO: days with hours
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -515,8 +532,101 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
<section class='mx-auto max-w-7xl my-8'>
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
{view === 'day'
|
{view === 'day'
|
||||||
? (
|
? (
|
||||||
<section>
|
<section class='shadow-md lg:flex lg:flex-auto lg:flex-col rounded-md'>
|
||||||
TODO: Build day view
|
<section class='border-b border-slate-500 bg-slate-700 text-center text-base font-semibold text-white lg:flex-none rounded-t-md'>
|
||||||
|
<div class='flex justify-center bg-gray-900 py-2 rounded-t-md'>
|
||||||
|
<span>{dayFormat.format(new Date(startDate))}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class='flex bg-slate-500 text-sm text-white lg:flex-auto rounded-b-md'>
|
||||||
|
<section class='w-full rounded-b-md'>
|
||||||
|
{hours.map((hour, hourIndex) => {
|
||||||
|
const shortIsoDate = hour.date.toISOString().substring(0, 10);
|
||||||
|
|
||||||
|
const startHourDate = new Date(shortIsoDate);
|
||||||
|
startHourDate.setHours(hour.date.getHours());
|
||||||
|
const endHourDate = new Date(shortIsoDate);
|
||||||
|
endHourDate.setHours(hour.date.getHours());
|
||||||
|
endHourDate.setMinutes(59);
|
||||||
|
endHourDate.setSeconds(59);
|
||||||
|
endHourDate.setMilliseconds(999);
|
||||||
|
|
||||||
|
const isLastHour = hourIndex === 23;
|
||||||
|
|
||||||
|
const hourEvents = calendarEvents.value.filter((calendarEvent) => {
|
||||||
|
const eventStartDate = new Date(calendarEvent.start_date);
|
||||||
|
const eventEndDate = new Date(calendarEvent.end_date);
|
||||||
|
eventEndDate.setSeconds(eventEndDate.getSeconds() - 1); // Take one second back so events don't bleed into the next hour
|
||||||
|
|
||||||
|
// Event starts and ends on this hour
|
||||||
|
if (eventStartDate >= startHourDate && eventEndDate <= endHourDate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event starts before and ends after this hour
|
||||||
|
if (eventStartDate <= startHourDate && eventEndDate >= endHourDate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event starts on and ends after this hour
|
||||||
|
if (
|
||||||
|
eventStartDate >= startHourDate && eventStartDate <= endHourDate && eventEndDate >= endHourDate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event starts before and ends on this hour
|
||||||
|
if (
|
||||||
|
eventStartDate <= startHourDate && eventEndDate >= startHourDate && eventEndDate <= endHourDate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
class={`relative ${hour.isCurrentHour ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
||||||
|
hour.isCurrentHour ? '' : 'text-slate-100'
|
||||||
|
} ${isLastHour ? 'rounded-b-md' : ''} border-b border-b-slate-600`}
|
||||||
|
>
|
||||||
|
<time datetime={startHourDate.toISOString()}>
|
||||||
|
{hourFormat.format(startHourDate)}
|
||||||
|
</time>
|
||||||
|
{hourEvents.length > 0
|
||||||
|
? (
|
||||||
|
<ol class='mt-2'>
|
||||||
|
{hourEvents.map((hourEvent) => (
|
||||||
|
<li class='mb-1'>
|
||||||
|
<a
|
||||||
|
href='javascript:void(0);'
|
||||||
|
class={`flex px-2 py-2 rounded-md hover:no-underline hover:opacity-60 ${
|
||||||
|
visibleCalendars.find((calendar) => calendar.id === hourEvent.calendar_id)
|
||||||
|
?.color || 'bg-gray-700'
|
||||||
|
}`}
|
||||||
|
onClick={() => openEvent.value = hourEvent}
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
datetime={new Date(hourEvent.start_date).toISOString()}
|
||||||
|
class='mr-2 flex-none text-slate-100 block'
|
||||||
|
>
|
||||||
|
{hourFormat.format(new Date(hourEvent.start_date))}
|
||||||
|
</time>
|
||||||
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
|
{hourEvent.title}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
@@ -529,126 +639,142 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
: null}
|
: null}
|
||||||
{view === 'month'
|
{view === 'month'
|
||||||
? (
|
? (
|
||||||
<section>
|
<section class='shadow-md lg:flex lg:flex-auto lg:flex-col rounded-md'>
|
||||||
<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 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'>
|
||||||
<div class='flex justify-center bg-gray-900 py-2 rounded-tl-md'>
|
<span>Mon</span>
|
||||||
<span>M</span>
|
</div>
|
||||||
<span class='sr-only sm:not-sr-only'>on</span>
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
</div>
|
<span>Tue</span>
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
</div>
|
||||||
<span>T</span>
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
<span class='sr-only sm:not-sr-only'>ue</span>
|
<span>Wed</span>
|
||||||
</div>
|
</div>
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
<span>W</span>
|
<span>Thu</span>
|
||||||
<span class='sr-only sm:not-sr-only'>ed</span>
|
</div>
|
||||||
</div>
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
<span>Fri</span>
|
||||||
<span>T</span>
|
</div>
|
||||||
<span class='sr-only sm:not-sr-only'>hu</span>
|
<div class='flex justify-center bg-gray-900 py-2'>
|
||||||
</div>
|
<span>Sat</span>
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
</div>
|
||||||
<span>F</span>
|
<div class='flex justify-center bg-gray-900 py-2 rounded-tr-md'>
|
||||||
<span class='sr-only sm:not-sr-only'>ri</span>
|
<span>Sun</span>
|
||||||
</div>
|
</div>
|
||||||
<div class='flex justify-center bg-gray-900 py-2'>
|
</section>
|
||||||
<span>S</span>
|
<section class='flex bg-slate-500 text-xs text-white lg:flex-auto rounded-b-md'>
|
||||||
<span class='sr-only sm:not-sr-only'>at</span>
|
<section class='w-full grid lg:grid-cols-7 lg:grid-rows-5 lg:gap-px rounded-b-md'>
|
||||||
</div>
|
{weeks.map((week, weekIndex) =>
|
||||||
<div class='flex justify-center bg-gray-900 py-2 rounded-tr-md'>
|
week.map((day, dayIndex) => {
|
||||||
<span>S</span>
|
const shortIsoDate = day.date.toISOString().substring(0, 10);
|
||||||
<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
|
const dayEvents = calendarEvents.value.filter((calendarEvent) => {
|
||||||
const dayEvents = calendarEvents.value.filter((calendarEvent) =>
|
const eventStartDate = new Date(calendarEvent.start_date);
|
||||||
new Date(calendarEvent.start_date) >= startDayDate &&
|
const eventEndDate = new Date(calendarEvent.end_date);
|
||||||
new Date(calendarEvent.end_date) <= endDayDate
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
// Event starts and ends on this day
|
||||||
<section
|
if (eventStartDate >= startDayDate && eventEndDate <= endDayDate) {
|
||||||
class={`relative ${day.isSameMonth ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
return true;
|
||||||
day.isSameMonth ? '' : 'text-slate-100'
|
}
|
||||||
} ${isBottomLeftDay ? 'rounded-bl-md' : ''} ${isBottomRightDay ? 'rounded-br-md' : ''}`}
|
|
||||||
|
// Event starts before and ends after this day
|
||||||
|
if (eventStartDate <= startDayDate && eventEndDate >= endDayDate) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event starts on and ends after this day
|
||||||
|
if (
|
||||||
|
eventStartDate >= startDayDate && eventStartDate <= endDayDate && eventEndDate >= endDayDate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event starts before and ends on this day
|
||||||
|
if (
|
||||||
|
eventStartDate <= startDayDate && eventEndDate >= startDayDate && eventEndDate <= endDayDate
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
class={`relative ${day.isSameMonth ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
||||||
|
day.isSameMonth ? '' : 'text-slate-100'
|
||||||
|
} ${isBottomLeftDay ? 'rounded-bl-md' : ''} ${isBottomRightDay ? 'rounded-br-md' : ''}`}
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
datetime={shortIsoDate}
|
||||||
|
class={`${
|
||||||
|
isToday
|
||||||
|
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<time
|
{day.date.getDate()}
|
||||||
datetime={shortIsoDate}
|
</time>
|
||||||
class={`${
|
{dayEvents.length > 0
|
||||||
isToday
|
? (
|
||||||
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
<ol class='mt-2'>
|
||||||
: ''
|
{[...dayEvents].slice(0, 2).map((dayEvent) => (
|
||||||
}`}
|
<li class='mb-1'>
|
||||||
>
|
<a
|
||||||
{day.date.getDate()}
|
href='javascript:void(0);'
|
||||||
</time>
|
class={`flex px-2 py-1 rounded-md hover:no-underline hover:opacity-60 ${
|
||||||
{dayEvents.length > 0
|
visibleCalendars.find((calendar) => calendar.id === dayEvent.calendar_id)
|
||||||
? (
|
?.color || 'bg-gray-700'
|
||||||
<ol class='mt-2'>
|
}`}
|
||||||
{[...dayEvents].slice(0, 2).map((dayEvent) => (
|
onClick={() => openEvent.value = dayEvent}
|
||||||
|
>
|
||||||
|
<time
|
||||||
|
datetime={new Date(dayEvent.start_date).toISOString()}
|
||||||
|
class='mr-2 flex-none text-slate-100 block'
|
||||||
|
>
|
||||||
|
{hourFormat.format(new Date(dayEvent.start_date))}
|
||||||
|
</time>
|
||||||
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
|
{dayEvent.title}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{dayEvents.length > 2
|
||||||
|
? (
|
||||||
<li class='mb-1'>
|
<li class='mb-1'>
|
||||||
<a
|
<a
|
||||||
href='javascript:void(0);'
|
href={`/calendar/view=day&startDate=${shortIsoDate}`}
|
||||||
class={`flex px-2 py-0 rounded-md hover:no-underline hover:opacity-60 ${
|
class='flex bg-gray-700 px-2 py-1 rounded-md hover:no-underline hover:opacity-60'
|
||||||
visibleCalendars.find((calendar) => calendar.id === dayEvent.calendar_id)
|
target='_blank'
|
||||||
?.color || 'bg-gray-700'
|
|
||||||
}`}
|
|
||||||
onClick={() => openEvent.value = dayEvent}
|
|
||||||
>
|
>
|
||||||
<time
|
|
||||||
datetime={new Date(dayEvent.start_date).toISOString()}
|
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
|
||||||
>
|
|
||||||
{hourFormat.format(new Date(dayEvent.start_date))}
|
|
||||||
</time>
|
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
{dayEvent.title}
|
...{dayEvents.length - 2} more event{dayEvents.length - 2 === 1 ? '' : 's'}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)
|
||||||
{dayEvents.length > 2
|
: null}
|
||||||
? (
|
</ol>
|
||||||
<li class='mb-1'>
|
)
|
||||||
<a
|
: null}
|
||||||
href={`/calendar/view=day&startDate=${shortIsoDate}`}
|
</section>
|
||||||
class='flex bg-gray-700 px-2 py-0 rounded-md hover:no-underline hover:opacity-60'
|
);
|
||||||
target='_blank'
|
})
|
||||||
>
|
)}
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
|
||||||
...{dayEvents.length - 2} more event{dayEvents.length - 2 === 1 ? '' : 's'}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</ol>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user