diff --git a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx index 096d57bf36..4b49fdb79d 100644 --- a/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx +++ b/packages/features/timezone-buddy/components/AvailabilitySliderTable.tsx @@ -1,6 +1,6 @@ import { keepPreviousData } from "@tanstack/react-query"; -import { getCoreRowModel, useReactTable, getFilteredRowModel } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table"; +import { getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import dayjs from "@calcom/dayjs"; @@ -15,6 +15,7 @@ import { Button, ButtonGroup, DataTable, DataTableToolbar, UserAvatar } from "@c import { UpgradeTip } from "../../tips/UpgradeTip"; import { createTimezoneBuddyStore, TBContext } from "../store"; import { AvailabilityEditSheet } from "./AvailabilityEditSheet"; +import { CellHighlightContainer } from "./CellHighlightContainer"; import { TimeDial } from "./TimeDial"; export interface SliderUser { @@ -64,6 +65,10 @@ export function AvailabilitySliderTable(props: { userTimeFormat: number | null; const [editSheetOpen, setEditSheetOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); + const tbStore = createTimezoneBuddyStore({ + browsingDate: browsingDate.toDate(), + }); + const { data, isPending, fetchNextPage, isFetching } = trpc.viewer.availability.listTeam.useInfiniteQuery( { limit: 10, @@ -205,7 +210,7 @@ export function AvailabilitySliderTable(props: { userTimeFormat: number | null; browsingDate: browsingDate.toDate(), })}> <> -
+ -
+ {selectedUser && editSheetOpen ? ( (null); + const { x, y, height, isHover, updateDimensions, setContainerRef } = useStore(store, (state) => state); + + useEffect(() => { + let timeout: NodeJS.Timeout | null = null; + if (isHover) { + setIsAnimating(true); + } else { + timeout = setTimeout(() => setIsAnimating(false), 1000); + } + return () => { + timeout && clearTimeout(timeout); + }; + }, [isHover]); + + useLayoutEffect(() => { + const handleUpdate = () => { + updateDimensions(); + }; + + const resizeObserver = new ResizeObserver(() => handleUpdate()); + + const currentContainerRef = componentContainerRef.current; + setContainerRef(componentContainerRef); + if (currentContainerRef) { + resizeObserver.observe(currentContainerRef); + } + + return () => { + if (currentContainerRef) { + resizeObserver.unobserve(currentContainerRef); + } + }; + }, [componentContainerRef, setContainerRef, updateDimensions]); + + return ( + +
+ {children} + + +
+
+ ); +} diff --git a/packages/features/timezone-buddy/components/TimeDial.tsx b/packages/features/timezone-buddy/components/TimeDial.tsx index 9c5daf43d0..29d558a883 100644 --- a/packages/features/timezone-buddy/components/TimeDial.tsx +++ b/packages/features/timezone-buddy/components/TimeDial.tsx @@ -1,3 +1,4 @@ +import type { MouseEventHandler } from "react"; import { useContext } from "react"; import { useStore } from "zustand"; @@ -86,7 +87,10 @@ function isCurrentHourInRange({ export function TimeDial({ timezone, dateRanges }: TimeDialProps) { const store = useContext(TBContext); if (!store) throw new Error("Missing TBContext.Provider in the tree"); - const browsingDate = useStore(store, (s) => s.browsingDate); + const { browsingDate, emitCellPosition } = useStore(store, ({ browsingDate, emitCellPosition }) => ({ + browsingDate, + emitCellPosition, + })); const usersTimezoneDate = dayjs(browsingDate).tz(timezone); @@ -102,16 +106,27 @@ export function TimeDial({ timezone, dateRanges }: TimeDialProps) { hours.filter((i) => i >= 24).map((i) => i % 24), ]; - let minuteOffsetApplied = false; + const handleMouseEnter: MouseEventHandler = (e) => { + const target = e.currentTarget as HTMLElement; + const rect = target.getBoundingClientRect(); + const x = rect.left; // x position within the element + const y = rect.top; // y position within the element + emitCellPosition(x); + }; return ( <> -
+
{days.map((day, i) => { if (!day.length) return null; const dateWithDaySet = usersTimezoneDate.add(i - 1, "day"); return ( -
+
{day.map((h) => { const hours = Math.floor(h); // Whole number part @@ -154,11 +169,14 @@ export function TimeDial({ timezone, dateRanges }: TimeDialProps) { } } - const minuteOffsetStyles: { marginLeft?: string } = {}; - if (hours !== 0 && !minuteOffsetApplied) { - minuteOffsetApplied = true; - minuteOffsetStyles.marginLeft = `${DAY_CELL_WIDTH * (offset % 1)}px`; - } + const TimeLabel = () => ( + <> + {hourSet.format("H")} + {minutes !== 0 && ( + {hourSet.format("mm")} + )} + + ); return (
emitCellPosition(-1)} style={{ - ...minuteOffsetStyles, width: `${DAY_CELL_WIDTH}px`, backgroundImage: rangeGradients.backgroundGradient, }}> {hours ? (
-
+
{rangeGradients.textGradient ? ( <> {/* light mode */} - - {hourSet.format("H")} + + {/* dark mode */} ) : ( - {hourSet.format("H")} + + + )}
diff --git a/packages/features/timezone-buddy/store.ts b/packages/features/timezone-buddy/store.ts index 65e1b0ccce..bb0718e145 100644 --- a/packages/features/timezone-buddy/store.ts +++ b/packages/features/timezone-buddy/store.ts @@ -13,12 +13,20 @@ export interface Timezone { export interface TimezoneBuddyProps { browsingDate: Date; timeMode?: "12h" | "24h"; + containerRef?: React.RefObject; + x: number; + height: number; + y: number; + isHover?: boolean; } type TimezoneBuddyState = TimezoneBuddyProps & { addToDate: (amount: number) => void; subtractFromDate: (amount: number) => void; setBrowseDate: (date: Date) => void; + setContainerRef: (ref: React.RefObject) => void; + emitCellPosition: (x: number) => void; + updateDimensions: () => void; }; export type TimezoneBuddyStore = ReturnType; @@ -32,6 +40,9 @@ export const createTimezoneBuddyStore = (initProps?: Partial const DEFAULT_PROPS: TimezoneBuddyProps = { timeMode: "24h", browsingDate: new Date(), + x: 0, + y: 0, + height: 0, }; return createStore()((set, get) => ({ @@ -50,6 +61,33 @@ export const createTimezoneBuddyStore = (initProps?: Partial setBrowseDate: (date: Date) => { set({ browsingDate: date }); }, + setContainerRef: (ref: React.RefObject) => { + set({ containerRef: ref }); + }, + emitCellPosition: (x: number) => { + const container = get().containerRef?.current; + if (x < 0) { + // If x is less than 0, we are outside the container + set({ isHover: false }); + } else if (container) { + const containerRect = container.getBoundingClientRect(); + set({ x: x - containerRect.left, isHover: true }); + } + }, + updateDimensions: () => { + const container = get().containerRef?.current; + let x = get().x; + if (container) { + const containerRect = container.getBoundingClientRect(); + const timeDials = container.querySelectorAll("[data-time-dial]>div"); + const height = + timeDials[timeDials.length - 1]?.getBoundingClientRect().bottom - + timeDials[0]?.getBoundingClientRect().top; + const y = timeDials[0]?.getBoundingClientRect().top - containerRect.top; + x = x ? x : timeDials[0]?.getBoundingClientRect().left - containerRect.left; + set({ height, y, x }); + } + }, })); };