Skip to content

Commit

Permalink
Calendar: overview fixes (#186)
Browse files Browse the repository at this point in the history
* overview bugs

* dbl click + month picker

* show full week button (+ refactoring)

* view dropdown

* fix new stay start date bug

* new keyboard shortcuts

* finish overview refactoring
  • Loading branch information
hingobway authored Oct 20, 2024
1 parent 76043f0 commit 90ba29b
Show file tree
Hide file tree
Showing 20 changed files with 597 additions and 326 deletions.
5 changes: 5 additions & 0 deletions client/src/CONSTANTS.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ViewType } from './app/calendar/_util/queryStates';

/** standard number of times to retry a query before failing */
export const GLOBAL_RETRIES = 3;

Expand All @@ -13,3 +15,6 @@ export const CALENDAR_DAYS_MAX = 14;
// the default number of days to show on smaller and larger screens
export const CALENDAR_DEFAULT_DAYS_DESKTOP = 7;
export const CALENDAR_DEFAULT_DAYS_MOBILE = 4;

// the default view to show in the calendar
export const CALENDAR_DEFAULT_VIEW: ViewType = 'OVERVIEW';
58 changes: 58 additions & 0 deletions client/src/app/_ctx/globalKeyboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use client';

import { useEffect } from 'react';

/**
* pass a handler function to assign keyboard shortcuts, most likely
* using `e.code` to match for actions.
*
* YOU SHOULD DEDUPE THE HANDLER FUNCTION to avoid reassigning on every render (see example).
*
* @example
* const handleShortcut = useCallback<GlobalKeyboardHandler>(
* (e, { withModifiers }) => {
* switch (e.code) {
* case 'KeyP':
* if (withModifiers) break;
* runMyAction();
* break;
* }
* },
* [runMyAction],
* );
* useGlobalKeyboardShortcuts(handleShortcut);
*/
export function useGlobalKeyboardShortcuts(handler: GlobalKeyboardHandler) {
useEffect(() => {
const dom = window.document;
if (!dom) return;

const cb = (e: KeyboardEvent) => {
// make sure user isn't typing
const target = e.target as HTMLElement;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.isContentEditable
)
return;

handler(e, { withModifiers: withModifiers(e) });
};

// attach event listener
dom.addEventListener('keydown', cb);
return () => dom.removeEventListener('keydown', cb);
}, [handler]);
}

// ---------------------------

export type GlobalKeyboardHandler = (
e: KeyboardEvent,
opts: { withModifiers: boolean },
) => void;

export function withModifiers(e: KeyboardEvent) {
return e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;
}
5 changes: 3 additions & 2 deletions client/src/app/calendar/_components/Agenda.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import {
IconPoint,
} from '@tabler/icons-react';

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

import EventPopup from './EventPopup';
Expand Down
54 changes: 20 additions & 34 deletions client/src/app/calendar/_components/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import { Inside } from '@/util/inferTypes';
import { useDefaultDays } from '../_util/defaultDays';
import { createCallbackCtx } from '@/app/_ctx/callback';
import { useCalendarControls } from '../_util/controls';
import { useCalendarView, useDisplayByRooms } from '../_util/displayByRooms';
import { useCalendarView, useDisplayByRooms } from '../_util/queryStates';
import { SetState } from '@/util/stateType';

import Timeline from './Timeline';
import Controls from './Controls';
import Agenda from './Agenda';
import Overview, { OVERVIEW_NUM_WEEKS } from './Overview';
import {
GlobalKeyboardHandler,
useGlobalKeyboardShortcuts,
} from '@/app/_ctx/globalKeyboard';

export const EVENTS_QUERY = graphql(`
query Stays($start: Int!, $end: Int!) {
Expand Down Expand Up @@ -63,7 +67,7 @@ export const { Provider: InvalidateProvider, useHook: useInvalidate } =
createCallbackCtx();

/** query parameters */
export type QP = 'date' | 'days';
export type QP = 'date' | 'days' | 'rooms' | 'view';

// COMPONENT
export default function Calendar() {
Expand All @@ -77,7 +81,7 @@ export default function Calendar() {
const defaultDays = useDefaultDays();
const days = useMemo(() => {
if (view === 'OVERVIEW') return 31;
const num = parseInt(sq.get('days' as QP) ?? '');
const num = parseInt(sq.get('days' satisfies QP) ?? '');
if (!Number.isFinite(num)) return undefined;
return num;
}, [sq, view]);
Expand All @@ -88,7 +92,7 @@ export default function Calendar() {

// date picker state
const startDate = useMemo(() => {
const num = parseInt(sq.get('date' as QP) ?? '');
const num = parseInt(sq.get('date' satisfies QP) ?? '');
let startDateNum = Number.isFinite(num) ? num : null;
if (startDateNum === null) {
if (daysWithDefault !== 7) return new Date();
Expand All @@ -107,8 +111,8 @@ export default function Calendar() {

function updateQuery(key: QP, val: string | number) {
const query = new URLSearchParams(sq);
if (days && view !== 'OVERVIEW') query.set('days' as QP, '' + days);
query.set('date' as QP, '' + dateTS(startDate));
if (days && view !== 'OVERVIEW') query.set('days' satisfies QP, '' + days);
query.set('date' satisfies QP, '' + dateTS(startDate));
query.set(key, '' + val);
router.push('?' + query.toString(), { scroll: false });
}
Expand Down Expand Up @@ -176,48 +180,30 @@ export default function Calendar() {

// KEYBOARD SHORTCUTS
const actions = useCalendarControls(props);
useEffect(() => {
const dom = window.document;
if (!dom) return;

const withModifiers = (e: KeyboardEvent) =>
e.ctrlKey || e.shiftKey || e.altKey || e.metaKey;

const cb = (e: KeyboardEvent) => {
// make sure user isn't typing
const target = e.target as HTMLElement;
if (
target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target?.isContentEditable
)
return;

// handle keyboard shortcuts
const keyboardHandler = useCallback<GlobalKeyboardHandler>(
(e, { withModifiers }) => {
switch (e.code) {
case 'KeyP':
if (withModifiers(e)) break;
if (withModifiers) break;
actions.last();
break;
case 'KeyN':
if (withModifiers(e)) break;
if (withModifiers) break;
actions.next();
break;
case 'KeyT':
if (withModifiers(e)) break;
if (withModifiers) break;
actions.today();
break;
case 'KeyR':
if (withModifiers(e)) break;
if (withModifiers) break;
toggleDisplayByRoom();
break;
}
};

// attach event listener
dom.addEventListener('keydown', cb);
return () => dom.removeEventListener('keydown', cb);
}, [actions, toggleDisplayByRoom]);
},
[actions, toggleDisplayByRoom],
);
useGlobalKeyboardShortcuts(keyboardHandler);

return (
<>
Expand Down
78 changes: 63 additions & 15 deletions client/src/app/calendar/_components/Controls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState, useTransition } from 'react';

import { Transition, TransitionChild } from '@headlessui/react';
import { MenuButton, Transition, TransitionChild } from '@headlessui/react';
import {
ActionIcon,
ActionIconProps,
Expand All @@ -10,15 +10,19 @@ import {
PopoverTarget,
Tooltip,
} from '@mantine/core';
import { DatePicker } from '@mantine/dates';
import { DatePicker, MonthPicker } from '@mantine/dates';
import {
IconArrowLeft,
IconArrowRight,
IconCalendarMonth,
IconCheck,
IconChevronDown,
IconLayoutList,
IconLibraryMinus,
IconLibraryPlus,
IconListDetails,
IconLoader2,
IconPlus,
IconSortAscendingShapes,
IconStackPop,
IconStackPush,
IconTable,
Expand All @@ -31,17 +35,21 @@ import { clamp } from '@/util/math';
import { useDefaultDays } from '../_util/defaultDays';
import { useReverseCbTrigger } from '@/util/reverseCb';
import { useCalendarControls } from '../_util/controls';
import { useCalendarView, useDisplayByRooms } from '../_util/displayByRooms';
import { useCalendarView, useDisplayByRooms } from '../_util/queryStates';
import { IconType } from '@/util/iconType';
import { CALENDAR_DAYS_MAX, CALENDAR_DAYS_MIN } from '@/CONSTANTS';

import EventEditWindow from './EventEditWindow';
import DKbd from '@/app/_components/_base/DKbd';
import {
Dropdown,
DropdownItems,
DropdownOption,
} from '@/app/_components/_base/Dropdown';

export default function Controls(props: CalendarProps) {
const {
isLoading,
dates,
selectedDate,
periodState: { days, setDays, startDate, setStartDate },
roomCollapse: {
Expand All @@ -68,11 +76,13 @@ export default function Controls(props: CalendarProps) {
roomLoading(async () => setDisplayByRoom(nv));
}

const [view, , nextView] = useCalendarView();
const [view, setView] = useCalendarView();

// new stay prompt
const { prop: newStay, trigger: openNewStay } = useReverseCbTrigger();

const Picker = view === 'OVERVIEW' ? MonthPicker : DatePicker;

return (
<>
<div className="flex flex-row flex-wrap items-center justify-between gap-y-4">
Expand All @@ -93,7 +103,7 @@ export default function Controls(props: CalendarProps) {
</Tooltip>
</PopoverTarget>
<PopoverDropdown>
<DatePicker
<Picker
value={startDate}
date={dateShown}
onDateChange={setDateShown}
Expand Down Expand Up @@ -256,15 +266,53 @@ export default function Controls(props: CalendarProps) {
</div>

{/* view type */}
<Tooltip label="Change view">
<ActionIcon
variant="subtle"
<Dropdown>
<Button
aria-label="change view"
component={MenuButton}
color="slate"
onClick={() => nextView()}
size="compact-sm"
justify="center"
variant="subtle"
rightSection={<IconChevronDown className="size-4" />}
>
<IconSortAscendingShapes />
</ActionIcon>
</Tooltip>
View
</Button>

<DropdownItems className="z-[999]">
<div className="py-1">
<DropdownOption
icon={IconCalendarMonth}
onClick={() => setView('OVERVIEW')}
>
<span className="flex-1 text-left">Month View</span>
{view === 'OVERVIEW' && (
<IconCheck stroke={1.5} className="text-slate-600" />
)}
</DropdownOption>

<DropdownOption
icon={IconLayoutList}
onClick={() => setView('TIMELINE')}
>
<span className="flex-1 text-left">Timeline View</span>
{view === 'TIMELINE' && (
<IconCheck stroke={1.5} className="text-slate-600" />
)}
</DropdownOption>

<DropdownOption
icon={IconListDetails}
onClick={() => setView('AGENDA')}
>
<span className="flex-1 text-left">Arrivals/Departures</span>
{view === 'AGENDA' && (
<IconCheck stroke={1.5} className="text-slate-600" />
)}
</DropdownOption>
</div>
</DropdownItems>
</Dropdown>

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

Expand All @@ -283,7 +331,7 @@ export default function Controls(props: CalendarProps) {
{/* popups */}
<EventEditWindow
trigger={newStay}
showDate={new Date(dateTSLocal(dates.start) * 1000)}
showDate={new Date(dateTSLocal(selectedDate) * 1000)}
/>
</div>
</div>
Expand Down
Loading

0 comments on commit 90ba29b

Please sign in to comment.