Dynamically render calendar with mock data
This commit is contained in:
@@ -2,7 +2,7 @@ import { useSignal } from '@preact/signals';
|
|||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
import { Calendar, CalendarEvent } from '/lib/types.ts';
|
import { Calendar, CalendarEvent } from '/lib/types.ts';
|
||||||
import { baseUrl, capitalizeWord } from '/lib/utils.ts';
|
import { baseUrl, capitalizeWord, getWeeksForMonth } from '/lib/utils.ts';
|
||||||
// import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/contacts/get.tsx';
|
// import { RequestBody as GetRequestBody, ResponseBody as GetResponseBody } from '/routes/api/contacts/get.tsx';
|
||||||
// import { RequestBody as AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/contacts/add.tsx';
|
// import { RequestBody as AddRequestBody, ResponseBody as AddResponseBody } from '/routes/api/contacts/add.tsx';
|
||||||
// import { RequestBody as DeleteRequestBody, ResponseBody as DeleteResponseBody } from '/routes/api/contacts/delete.tsx';
|
// import { RequestBody as DeleteRequestBody, ResponseBody as DeleteResponseBody } from '/routes/api/contacts/delete.tsx';
|
||||||
@@ -28,6 +28,8 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
|
||||||
|
|
||||||
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 today = new Date().toISOString().substring(0, 10);
|
||||||
|
|
||||||
function onClickAddEvent() {
|
function onClickAddEvent() {
|
||||||
if (isAdding.value) {
|
if (isAdding.value) {
|
||||||
@@ -113,7 +115,6 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onClickChangeStartDate(changeTo: 'previous' | 'next' | 'today') {
|
function onClickChangeStartDate(changeTo: 'previous' | 'next' | 'today') {
|
||||||
const today = new Date().toISOString().substring(0, 10);
|
|
||||||
const previousDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() - 1)).toISOString()
|
const previousDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() - 1)).toISOString()
|
||||||
.substring(0, 10);
|
.substring(0, 10);
|
||||||
const nextDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() + 1)).toISOString()
|
const nextDay = new Date(new Date(startDate).setUTCDate(new Date(startDate).getUTCDate() + 1)).toISOString()
|
||||||
@@ -315,6 +316,11 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
|
|
||||||
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
|
const visibleCalendars = calendars.value.filter((calendar) => calendar.is_visible);
|
||||||
|
|
||||||
|
const visibleCalendarEvents = calendarEvents.value;
|
||||||
|
|
||||||
|
// TODO: Send in / consider user timezone
|
||||||
|
const weeks = getWeeksForMonth(new Date(startDate));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section class='flex flex-row items-center justify-between mb-4'>
|
<section class='flex flex-row items-center justify-between mb-4'>
|
||||||
@@ -497,7 +503,7 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
<section class='mx-auto max-w-7xl my-8'>
|
<section class='mx-auto max-w-7xl my-8'>
|
||||||
<section>
|
<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'>
|
||||||
<div 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>M</span>
|
<span>M</span>
|
||||||
<span class='sr-only sm:not-sr-only'>on</span>
|
<span class='sr-only sm:not-sr-only'>on</span>
|
||||||
@@ -526,165 +532,94 @@ export default function MainCalendar({ initialCalendars, initialCalendarEvents,
|
|||||||
<span>S</span>
|
<span>S</span>
|
||||||
<span class='sr-only sm:not-sr-only'>un</span>
|
<span class='sr-only sm:not-sr-only'>un</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class='flex bg-slate-500 text-xs leading-6 text-white lg:flex-auto rounded-b-md'>
|
<section class='flex bg-slate-500 text-xs leading-6 text-white lg:flex-auto rounded-b-md'>
|
||||||
<div class='w-full grid lg:grid-cols-7 lg:grid-rows-5 lg:gap-px rounded-b-md'>
|
<section class='w-full grid lg:grid-cols-7 lg:grid-rows-5 lg:gap-px rounded-b-md'>
|
||||||
<div class='relative bg-slate-700 px-3 py-2 text-slate-100'>
|
{weeks.map((week, weekIndex) =>
|
||||||
<time datetime='2024-02-28'>26</time>
|
week.map((day, dayIndex) => {
|
||||||
</div>
|
const shortIsoDate = day.date.toISOString().substring(0, 10);
|
||||||
<div class='relative bg-slate-700 px-3 py-2 text-slate-100'>
|
|
||||||
<time datetime='2024-02-28'>27</time>
|
const startDayDate = new Date(shortIsoDate);
|
||||||
</div>
|
const endDayDate = new Date(shortIsoDate);
|
||||||
<div class='relative bg-slate-700 px-3 py-2 text-slate-100'>
|
endDayDate.setHours(23);
|
||||||
<time datetime='2024-02-28'>28</time>
|
endDayDate.setMinutes(59);
|
||||||
</div>
|
endDayDate.setSeconds(59);
|
||||||
<div class='relative bg-slate-700 px-3 py-2 text-slate-100'>
|
endDayDate.setMilliseconds(999);
|
||||||
<time datetime='2024-02-29'>29</time>
|
|
||||||
</div>
|
const isBottomLeftDay = weekIndex === weeks.length - 1 && dayIndex === 0;
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
const isBottomRightDay = weekIndex === weeks.length - 1 && dayIndex === week.length - 1;
|
||||||
<time datetime='2024-03-01'>1</time>
|
|
||||||
</div>
|
const isToday = today === shortIsoDate;
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-02'>2</time>
|
// TODO: Consider events that span multiple days
|
||||||
</div>
|
const dayEvents = calendarEvents.value.filter((calendarEvent) =>
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
new Date(calendarEvent.start_date) >= startDayDate &&
|
||||||
<time datetime='2024-03-03'>3</time>
|
new Date(calendarEvent.end_date) <= endDayDate
|
||||||
</div>
|
);
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-04'>4</time>
|
return (
|
||||||
</div>
|
<section
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
class={`relative ${day.isSameMonth ? 'bg-slate-600' : 'bg-slate-700'} min-h-16 px-3 py-2 ${
|
||||||
<time datetime='2024-03-05'>5</time>
|
day.isSameMonth ? '' : 'text-slate-100'
|
||||||
</div>
|
} ${isBottomLeftDay ? 'rounded-bl-md' : ''} ${isBottomRightDay ? 'rounded-br-md' : ''}`}
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-06'>6</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-07'>7</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-08'>8</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-09'>9</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-10'>10</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-11'>11</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-12'>12</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-13'>13</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-14'>14</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-15'>15</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-16'>16</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time
|
|
||||||
datetime='2024-03-17'
|
|
||||||
class='flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
|
||||||
>
|
|
||||||
17
|
|
||||||
</time>
|
|
||||||
<ol class='mt-2'>
|
|
||||||
<li class='mb-1'>
|
|
||||||
<a
|
|
||||||
href='#'
|
|
||||||
class='flex bg-purple-600 px-2 py-0 rounded-md hover:no-underline hover:bg-purple-500'
|
|
||||||
>
|
>
|
||||||
<time
|
<time
|
||||||
datetime='2024-03-17T14:00'
|
datetime={shortIsoDate}
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
class={`${
|
||||||
|
isToday
|
||||||
|
? 'flex h-6 w-6 items-center justify-center rounded-full bg-[#51A4FB] font-semibold'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
14:00
|
{day.date.getDate()}
|
||||||
</time>
|
</time>
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
{dayEvents.length > 0
|
||||||
Dentist
|
? (
|
||||||
</p>
|
<ol class='mt-2'>
|
||||||
</a>
|
{[...dayEvents].slice(0, 2).map((dayEvent) => (
|
||||||
</li>
|
<li class='mb-1'>
|
||||||
<li class='mb-1'>
|
<a
|
||||||
<a
|
href='#'
|
||||||
href='#'
|
class={`flex px-2 py-0 rounded-md hover:no-underline hover:opacity-60 ${
|
||||||
class='flex bg-purple-600 px-2 py-0 rounded-md hover:no-underline hover:bg-purple-500'
|
visibleCalendars.find((calendar) => calendar.id === dayEvent.calendar_id)
|
||||||
>
|
?.color || 'bg-gray-700'
|
||||||
<time
|
}`}
|
||||||
datetime='2024-03-17T16:30'
|
>
|
||||||
class='mr-2 flex-none text-slate-100 block'
|
<time
|
||||||
>
|
datetime={new Date(dayEvent.start_date).toISOString()}
|
||||||
16:30
|
class='mr-2 flex-none text-slate-100 block'
|
||||||
</time>
|
>
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
{hourFormat.format(new Date(dayEvent.start_date))}
|
||||||
Dermatologist
|
</time>
|
||||||
</p>
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
</a>
|
{dayEvent.title}
|
||||||
</li>
|
</p>
|
||||||
<li class='mb-1'>
|
</a>
|
||||||
<a
|
</li>
|
||||||
href='#'
|
))}
|
||||||
class='flex bg-purple-600 px-2 py-0 rounded-md hover:no-underline hover:bg-purple-500'
|
{dayEvents.length > 2
|
||||||
>
|
? (
|
||||||
<p class='flex-auto truncate font-medium text-white'>
|
<li class='mb-1'>
|
||||||
...3 more events
|
<a
|
||||||
</p>
|
href='#'
|
||||||
</a>
|
class='flex bg-purple-600 px-2 py-0 rounded-md hover:no-underline hover:bg-purple-500'
|
||||||
</li>
|
>
|
||||||
</ol>
|
<p class='flex-auto truncate font-medium text-white'>
|
||||||
</div>
|
...{dayEvents.length - 2} more event{dayEvents.length - 2 === 1 ? '' : 's'}
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
</p>
|
||||||
<time datetime='2024-03-18'>18</time>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
)
|
||||||
<time datetime='2024-03-19'>19</time>
|
: null}
|
||||||
</div>
|
</ol>
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
)
|
||||||
<time datetime='2024-03-20'>20</time>
|
: null}
|
||||||
</div>
|
</section>
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
);
|
||||||
<time datetime='2024-03-21'>21</time>
|
})
|
||||||
</div>
|
)}
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
</section>
|
||||||
<time datetime='2024-03-22'>22</time>
|
</section>
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-23'>23</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-24'>24</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-25'>25</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-26'>26</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-27'>27</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-28'>28</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-29'>29</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2'>
|
|
||||||
<time datetime='2024-03-30'>30</time>
|
|
||||||
</div>
|
|
||||||
<div class='relative bg-slate-600 px-3 py-2 rounded-br-md'>
|
|
||||||
<time datetime='2024-03-31'>31</time>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
37
lib/utils.ts
37
lib/utils.ts
@@ -628,3 +628,40 @@ export function parseVCardFromTextContents(text: string): Partial<Contact>[] {
|
|||||||
export const capitalizeWord = (string: string) => {
|
export const capitalizeWord = (string: string) => {
|
||||||
return `${string.charAt(0).toLocaleUpperCase()}${string.slice(1)}`;
|
return `${string.charAt(0).toLocaleUpperCase()}${string.slice(1)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NOTE: Considers weeks starting Monday, not Sunday
|
||||||
|
export function getWeeksForMonth(date: Date): { date: Date; isSameMonth: boolean }[][] {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
|
||||||
|
const firstOfMonth = new Date(year, month, 1);
|
||||||
|
const lastOfMonth = new Date(year, month + 1, 0);
|
||||||
|
|
||||||
|
const daysToShow = firstOfMonth.getDay() + (firstOfMonth.getDay() === 0 ? 6 : -1) + lastOfMonth.getDate();
|
||||||
|
|
||||||
|
const weekCount = Math.ceil(daysToShow / 7);
|
||||||
|
|
||||||
|
const weeks: { date: Date; isSameMonth: boolean }[][] = [];
|
||||||
|
|
||||||
|
const startingDate = new Date(firstOfMonth);
|
||||||
|
startingDate.setDate(
|
||||||
|
startingDate.getDate() - Math.abs(firstOfMonth.getDay() === 0 ? 6 : (firstOfMonth.getDay() - 1)),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let weekIndex = 0; weeks.length < weekCount; ++weekIndex) {
|
||||||
|
for (let dayIndex = 0; dayIndex < 7; ++dayIndex) {
|
||||||
|
if (!Array.isArray(weeks[weekIndex])) {
|
||||||
|
weeks[weekIndex] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekDayDate = new Date(startingDate);
|
||||||
|
weekDayDate.setDate(weekDayDate.getDate() + (dayIndex + weekIndex * 7));
|
||||||
|
|
||||||
|
const isSameMonth = weekDayDate.getMonth() === month;
|
||||||
|
|
||||||
|
weeks[weekIndex].push({ date: weekDayDate, isSameMonth });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return weeks;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,14 +8,82 @@ async function getCalendars(userId: string): Promise<Pick<Calendar, 'id' | 'name
|
|||||||
// TODO: Remove this
|
// TODO: Remove this
|
||||||
await new Promise((resolve) => setTimeout(() => resolve(true), 1));
|
await new Promise((resolve) => setTimeout(() => resolve(true), 1));
|
||||||
|
|
||||||
return [];
|
return [
|
||||||
|
{
|
||||||
|
id: 'family-1',
|
||||||
|
name: 'Family',
|
||||||
|
color: 'bg-purple-500',
|
||||||
|
is_visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'personal-1',
|
||||||
|
name: 'Personal',
|
||||||
|
color: 'bg-sky-600',
|
||||||
|
is_visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'house-chores-1',
|
||||||
|
name: 'House Chores',
|
||||||
|
color: 'bg-red-700',
|
||||||
|
is_visible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getCalendarEvents(userId: string, calendarIds: string[]): Promise<CalendarEvent[]> {
|
async function getCalendarEvents(userId: string, calendarIds: string[]): Promise<CalendarEvent[]> {
|
||||||
// TODO: Remove this
|
// TODO: Remove this
|
||||||
await new Promise((resolve) => setTimeout(() => resolve(true), 1));
|
await new Promise((resolve) => setTimeout(() => resolve(true), 1));
|
||||||
|
|
||||||
return [];
|
return [
|
||||||
|
{
|
||||||
|
id: 'event-1',
|
||||||
|
user_id: userId,
|
||||||
|
calendar_id: 'family-1',
|
||||||
|
revision: 'fake-rev',
|
||||||
|
title: 'Dentist',
|
||||||
|
start_date: new Date('2024-03-17T14:00:00.000Z'),
|
||||||
|
end_date: new Date('2024-03-17T15:00:00.000Z'),
|
||||||
|
is_all_day: false,
|
||||||
|
status: 'scheduled',
|
||||||
|
extra: {
|
||||||
|
visibility: 'default',
|
||||||
|
},
|
||||||
|
updated_at: new Date(),
|
||||||
|
created_at: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'event-2',
|
||||||
|
user_id: userId,
|
||||||
|
calendar_id: 'personal-1',
|
||||||
|
revision: 'fake-rev',
|
||||||
|
title: 'Dermatologist',
|
||||||
|
start_date: new Date('2024-03-17T16:30:00.000Z'),
|
||||||
|
end_date: new Date('2024-03-17T17:30:00.000Z'),
|
||||||
|
is_all_day: false,
|
||||||
|
status: 'scheduled',
|
||||||
|
extra: {
|
||||||
|
visibility: 'default',
|
||||||
|
},
|
||||||
|
updated_at: new Date(),
|
||||||
|
created_at: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'event-3',
|
||||||
|
user_id: userId,
|
||||||
|
calendar_id: 'house-chores-1',
|
||||||
|
revision: 'fake-rev',
|
||||||
|
title: 'Vacuum',
|
||||||
|
start_date: new Date('2024-03-16T15:00:00.000Z'),
|
||||||
|
end_date: new Date('2024-03-16T16:00:00.000Z'),
|
||||||
|
is_all_day: false,
|
||||||
|
status: 'scheduled',
|
||||||
|
extra: {
|
||||||
|
visibility: 'default',
|
||||||
|
},
|
||||||
|
updated_at: new Date(),
|
||||||
|
created_at: new Date(),
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
|
|||||||
Reference in New Issue
Block a user