feat(widgets): show calendar events on hover with pin on click (#2637) docs(widgets): update calendar interactions section for hover behavior
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import { useCallback, useState } from "react";
|
|
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
|
|
import { useElementSize } from "@mantine/hooks";
|
|
|
|
import { useRequiredBoard } from "@homarr/boards/context";
|
|
import type { CalendarEvent } from "@homarr/integrations/types";
|
|
|
|
import { CalendarEventList } from "./calendar-event-list";
|
|
|
|
interface CalendarDayProps {
|
|
date: Date;
|
|
events: CalendarEvent[];
|
|
disabled: boolean;
|
|
rootWidth: number;
|
|
rootHeight: number;
|
|
onOpen: (close: () => void) => void;
|
|
}
|
|
|
|
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => {
|
|
const [opened, setOpened] = useState(false);
|
|
const [pinned, setPinned] = useState(false);
|
|
const { primaryColor } = useMantineTheme();
|
|
const { ref, height } = useElementSize();
|
|
const board = useRequiredBoard();
|
|
const mantineTheme = useMantineTheme();
|
|
const actualItemRadius = mantineTheme.radius[board.itemRadius];
|
|
|
|
const minAxisSize = Math.min(rootWidth, rootHeight);
|
|
const shouldScaleDown = minAxisSize < 350;
|
|
const isSmall = rootHeight < 256;
|
|
|
|
const isTooSmallForIndicators = height < 30;
|
|
|
|
const handleMouseEnter = useCallback(() => {
|
|
if (disabled) return;
|
|
onOpen(() => {
|
|
setOpened(false);
|
|
setPinned(false);
|
|
});
|
|
setOpened(true);
|
|
}, [disabled, onOpen]);
|
|
|
|
const handleMouseLeave = useCallback(() => {
|
|
if (!pinned) setOpened(false);
|
|
}, [pinned]);
|
|
|
|
return (
|
|
<Popover
|
|
position="bottom"
|
|
withArrow
|
|
withinPortal
|
|
radius="lg"
|
|
shadow="sm"
|
|
transitionProps={{
|
|
transition: "pop",
|
|
}}
|
|
onChange={(value) => {
|
|
setOpened(value);
|
|
if (!value) setPinned(false);
|
|
}}
|
|
opened={opened}
|
|
disabled={disabled}
|
|
>
|
|
<Popover.Target>
|
|
<Container
|
|
h="100%"
|
|
w="100%"
|
|
p={0}
|
|
pt={isSmall ? 0 : 10}
|
|
pb={isSmall ? 0 : 10}
|
|
m={0}
|
|
ref={ref}
|
|
bd={`2px solid ${opened && !disabled ? primaryColor : "transparent"}`}
|
|
style={{
|
|
alignContent: "center",
|
|
borderRadius: actualItemRadius,
|
|
cursor: disabled ? "default" : "pointer",
|
|
}}
|
|
onClick={() => {
|
|
if (!disabled) setPinned((prev) => !prev);
|
|
}}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
|
|
{date.getDate()}
|
|
</Text>
|
|
{!isTooSmallForIndicators && <NotificationIndicator events={events} isSmall={isSmall} />}
|
|
</Container>
|
|
</Popover.Target>
|
|
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
|
|
<Popover.Dropdown
|
|
maw="calc(100vw - 24px)"
|
|
w={512}
|
|
pe={4}
|
|
pb={0}
|
|
style={{ overflow: "hidden" }}
|
|
onMouseEnter={() => setOpened(true)}
|
|
onMouseLeave={handleMouseLeave}
|
|
>
|
|
<CalendarEventList events={events} />
|
|
</Popover.Dropdown>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
interface NotificationIndicatorProps {
|
|
events: CalendarEvent[];
|
|
isSmall: boolean;
|
|
}
|
|
|
|
const NotificationIndicator = ({ events, isSmall }: NotificationIndicatorProps) => {
|
|
const notificationEvents = [...new Set(events.map((event) => event.indicatorColor))].filter(String);
|
|
/* position bottom is lower when small to not be on top of number*/
|
|
return (
|
|
<Flex
|
|
w="75%"
|
|
align={"center"}
|
|
pos={"absolute"}
|
|
gap={3}
|
|
bottom={isSmall ? 4 : 10}
|
|
left={"12.5%"}
|
|
p={0}
|
|
direction={"row"}
|
|
justify={"center"}
|
|
>
|
|
{notificationEvents.map((notificationEvent) => {
|
|
return <Box key={notificationEvent} bg={notificationEvent} h={4} w={4} p={0} style={{ borderRadius: 999 }} />;
|
|
})}
|
|
</Flex>
|
|
);
|
|
};
|