Skip to content

Commit

Permalink
Calendar (v6) overview view (#182)
Browse files Browse the repository at this point in the history
* init calendar overview

* swatch/responsive improvements
  • Loading branch information
hingobway authored Oct 20, 2024
1 parent 7d7a4be commit bbbb6b3
Show file tree
Hide file tree
Showing 15 changed files with 531 additions and 153 deletions.
65 changes: 65 additions & 0 deletions client/src/app/_components/_base/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { forwardRef } from 'react';
import { Menu, MenuItem, MenuItems } from '@headlessui/react';

import { clmx } from '@/util/classConcat';
import { IconType } from '@/util/iconType';

export const Dropdown = forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof Menu>
>(({ className, ...props }, ref) => {
return (
<Menu
ref={ref}
as="div"
className={clmx('relative inline-block text-left', className)}
{...props}
/>
);
});
Dropdown.displayName = 'Dropdown';

export const DropdownItems = forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<typeof MenuItems>
>(({ className, ...props }, ref) => {
return (
<MenuItems
ref={ref}
transition
className={clmx(
'absolute right-0 z-10 mt-2 flex w-56 flex-col divide-y divide-slate-200 rounded-md bg-slate-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none',
/* transition */ 'origin-top-right transition data-[closed]:scale-95 data-[closed]:transform data-[closed]:opacity-0 data-[enter]:duration-100 data-[leave]:duration-75 data-[enter]:ease-out data-[leave]:ease-in',

className,
)}
{...props}
/>
);
});
DropdownItems.displayName = 'DropdownItems';

export function DropdownOption({
icon: Icon,
className,
children,
...props
}: { icon: IconType } & JSX.IntrinsicElements['button']) {
return (
<MenuItem>
<button
className={clmx(
'group flex w-full items-center px-4 py-2 text-sm text-slate-700 data-[focus]:bg-slate-200 data-[focus]:text-slate-900',
className,
)}
{...props}
>
<Icon
aria-hidden="true"
className="mr-3 size-5 text-slate-400 group-hover:text-slate-500"
/>
{children}
</button>
</MenuItem>
);
}
54 changes: 5 additions & 49 deletions client/src/app/calendar/_components/Agenda.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,23 @@ import {
Fragment,
forwardRef,
useCallback,
useMemo,
useState,
} from 'react';

import {
Popover,
PopoverButton,
Transition,
TransitionChild,
} from '@headlessui/react';
import { Popover, PopoverButton, Transition } from '@headlessui/react';
import {
IconChevronDown,
IconMinus,
IconPlus,
IconPoint,
} from '@tabler/icons-react';

import {
UseDatesArrayProps,
dateFormat,
useDatesArray,
} from '../_util/dateUtils';
import { UseDatesArrayProps, dateFormat } from '../_util/dateUtils';
import { CalendarProps, EventType } from './Calendar';
import { clmx, clx } from '@/util/classConcat';
import { IconTypeProps } from '@/util/iconType';
import { alphabetical } from '@/util/sort';
import { useEventColorId } from '../_util/cabinColors';
import { useEventsByDay } from '../_util/eventsByDay';

import EventPopup from './EventPopup';
import RoomSwatch from './RoomSwatch';
Expand All @@ -40,35 +30,10 @@ import RoomSwatch from './RoomSwatch';
type AgendaProps = Pick<CalendarProps, 'events' | 'updatePeriod'> &
UseDatesArrayProps;
export default function Agenda({ ...props }: AgendaProps) {
const { events: events_in, updatePeriod } = props;
const { updatePeriod } = props;

// get days per event
const dates = useDatesArray(props);
const eventsByDay = useMemo<EventsByDay[] | null>(() => {
if (!events_in) return null;
const events = events_in.sort(alphabetical((s) => s.title));
return dates.map((d) => {
const ad = {
date: d,
count: events.filter(
(event) => event.dateStart <= d && event.dateEnd >= d,
).length,
arrivals: events.filter((event) => event.dateStart === d),
departures: events.filter((event) => event.dateEnd === d),
};
return {
...ad,
unchanged: events.filter(
(event) =>
event.dateStart <= d &&
event.dateEnd > d &&
!ad.arrivals.find((it) => it.id === event.id) &&
!ad.departures.find((it) => it.id === event.id),
),
noChanges: !ad.arrivals.length && !ad.departures.length,
};
});
}, [dates, events_in]);
const eventsByDay = useEventsByDay(props);

return (
<>
Expand Down Expand Up @@ -140,15 +105,6 @@ export default function Agenda({ ...props }: AgendaProps) {
);
}

type EventsByDay = {
date: number;
count: number;
noChanges: boolean;
arrivals: EventType[];
departures: EventType[];
unchanged: EventType[];
};

const subtleBtnStyles = clx(
'-mx-2 -my-0.5 rounded-md px-2 py-0.5 text-left focus:bg-slate-200 focus:outline-none hover:enabled:bg-slate-200/50',
);
Expand Down
45 changes: 32 additions & 13 deletions client/src/app/calendar/_components/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
dateStartOfWeek,
dateTS,
dateTSLocal,
dateTSObject,
dayjs,
} from '../_util/dateUtils';
import { Inside } from '@/util/inferTypes';
Expand All @@ -22,6 +23,7 @@ import { SetState } from '@/util/stateType';
import Timeline from './Timeline';
import Controls from './Controls';
import Agenda from './Agenda';
import Overview, { OVERVIEW_NUM_WEEKS } from './Overview';

export const EVENTS_QUERY = graphql(`
query Stays($start: Int!, $end: Int!) {
Expand Down Expand Up @@ -68,13 +70,17 @@ export default function Calendar() {
const sq = useSearchParams();
const router = useRouter();

// calendar view
const [view] = useCalendarView();

// days
const defaultDays = useDefaultDays();
const days = useMemo(() => {
if (view === 'OVERVIEW') return 31;
const num = parseInt(sq.get('days' as QP) ?? '');
if (!Number.isFinite(num)) return undefined;
return num;
}, [sq]);
}, [sq, view]);
const daysWithDefault = days ?? defaultDays;
function setDays(d: typeof days) {
updateQuery('days', d ?? '');
Expand All @@ -88,8 +94,10 @@ export default function Calendar() {
if (daysWithDefault !== 7) return new Date();
startDateNum = dateStartOfWeek(dateTS(new Date()));
}
if (view === 'OVERVIEW')
startDateNum = dateTSObject(startDateNum).startOf('month').unix();
return new Date(dateTSLocal(startDateNum) * 1000);
}, [daysWithDefault, sq]);
}, [daysWithDefault, sq, view]);
function setStartDate(d: typeof startDate) {
updateQuery('date', dateTS(d));
}
Expand All @@ -99,7 +107,7 @@ export default function Calendar() {

function updateQuery(key: QP, val: string | number) {
const query = new URLSearchParams(sq);
if (days) query.set('days' as QP, '' + days);
if (days && view !== 'OVERVIEW') query.set('days' as QP, '' + days);
query.set('date' as QP, '' + dateTS(startDate));
query.set(key, '' + val);
router.push('?' + query.toString(), { scroll: false });
Expand All @@ -112,13 +120,22 @@ export default function Calendar() {
.toDate(),
[daysWithDefault, startDate],
);
const parsedDates = useMemo(
() => ({
start: dateTS(startDate),
end: dateTS(endDate),
}),
[endDate, startDate],
);
const parsedDates = useMemo(() => {
let start = dateTS(startDate);
let end = dateTS(endDate);

if (view === 'OVERVIEW') {
const s = dateTSObject(start).startOf('month').startOf('week');
start = s.unix();
end = s.add(7 * OVERVIEW_NUM_WEEKS, 'days').unix();
}

return { start, end };
}, [endDate, startDate, view]);

const selectedDate = useMemo(() => {
return dateTS(startDate);
}, [startDate]);

// get calendar events
const query = useGraphQuery(EVENTS_QUERY, parsedDates);
Expand All @@ -136,9 +153,6 @@ export default function Calendar() {
const _dbr = useDisplayByRooms();
const toggleDisplayByRoom = useCallback(() => _dbr[1](!_dbr[0]), [_dbr]);

// calendar view
const [view] = useCalendarView();

// CALENDAR PROPS
const props: CalendarProps = {
events,
Expand All @@ -152,6 +166,7 @@ export default function Calendar() {
setStartDate,
setDays,
},
selectedDate,
roomCollapse: {
state: roomCollapse,
set: setRoomCollapse,
Expand Down Expand Up @@ -216,6 +231,9 @@ export default function Calendar() {

{/* agenda view */}
{view === 'AGENDA' && <Agenda {...props} />}

{/* overview view */}
{view === 'OVERVIEW' && <Overview {...props} />}
</div>
</InvalidateProvider>
</>
Expand All @@ -231,6 +249,7 @@ export type CalendarProps = {
start: number;
end: number;
};
selectedDate: number;
periodState: {
days: number | undefined;
setDays: (d: number | undefined) => void;
Expand Down
39 changes: 22 additions & 17 deletions client/src/app/calendar/_components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function Controls(props: CalendarProps) {
const {
isLoading,
dates,
selectedDate,
periodState: { days, setDays, startDate, setStartDate },
roomCollapse: {
full: [fullCollapse, setFullCollapse],
Expand Down Expand Up @@ -86,7 +87,7 @@ export default function Controls(props: CalendarProps) {
onClick={() => setDatePickerOpen(true)}
>
<h3 className="min-w-20 text-xl">
{dateFormat(dates.start, `MMM 'YY`)}
{dateFormat(selectedDate, `MMM 'YY`)}
</h3>
</button>
</Tooltip>
Expand Down Expand Up @@ -176,23 +177,27 @@ export default function Controls(props: CalendarProps) {
</div>

{/* number of days to show */}
<div className="flex flex-row items-center gap-1">
<input
type="text"
className="w-10 rounded-lg border border-transparent bg-transparent p-1 text-right hover:bg-slate-200 focus:border-slate-400 focus:bg-slate-200 focus:outline-none"
placeholder={'' + daysWithDefault}
value={days ?? ''}
onChange={({ currentTarget: { value: v } }) => {
if (!v.length) setDays(undefined);
const d = parseInt((v.match(/[\d\.]/g) || ['']).join(''));
if (Number.isFinite(d))
setDays(clamp(d, CALENDAR_DAYS_MIN, CALENDAR_DAYS_MAX));
}}
/>
<span>days</span>
</div>
{view !== 'OVERVIEW' && (
<>
<div className="flex flex-row items-center gap-1">
<input
type="text"
className="w-10 rounded-lg border border-transparent bg-transparent p-1 text-right hover:bg-slate-200 focus:border-slate-400 focus:bg-slate-200 focus:outline-none"
placeholder={'' + daysWithDefault}
value={days ?? ''}
onChange={({ currentTarget: { value: v } }) => {
if (!v.length) setDays(undefined);
const d = parseInt((v.match(/[\d\.]/g) || ['']).join(''));
if (Number.isFinite(d))
setDays(clamp(d, CALENDAR_DAYS_MIN, CALENDAR_DAYS_MAX));
}}
/>
<span>days</span>
</div>

<div className="self-stretch border-l border-slate-300"></div>
<div className="self-stretch border-l border-slate-300"></div>
</>
)}

{/* rooms controls */}
<div
Expand Down
Loading

0 comments on commit bbbb6b3

Please sign in to comment.