feat: weekly view calendar improvements (#9491)

* Add correct start/end time for weekly calendar by looking at calendar's availability

* Always show 24h in weekly calendar, added scroll functionality, current time and events in correct timezone

* Improved current time style

* Show slots for every minute, so also odd timeslots due to offsets work

* Show correct timeformat for weekly view on left side time stamps

* Fix data attr for debug

* Position events differently so we can accomodate for offset start times.

* Removed schedule from public event api, because we don't use it anymore

* Fixed alignment of timeslots in weekview calendar.

* Added loading spinner to weekly calendar

* Force weekly view calendar hour labels to show 00 minutes, also for gmt x.5 timezones that are offset 30 mins.

* Change event duration blocks in weekly calendar when user changes duration in multi duration event

* Improved week view slot time label alignments for shorter durations

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Jeroen ReumkensandPeer Richelsen authored and GitHub committed 2023-06-15 09:59:58 +00:00
1 parent faac3d0f57
commit 955de3133c
13 files changed
+233 -138

No files matched your search

+4
View File
@@ -96,9 +96,13 @@ module.exports = {
from: { opacity: 0, transform: "translateY(10px)" },
to: { opacity: 1, transform: "none" },
},
spinning: {
"100%": { transform: "rotate(360deg)" },
},
},
animation: {
"fade-in-up": "fade-in-up 600ms var(--animation-delay, 0ms) cubic-bezier(.21,1.02,.73,1) forwards",
spinning: "spinning 0.75s linear infinite",
},
boxShadow: {
dropdown: "0px 2px 6px -1px rgba(0, 0, 0, 0.08)",
+11 -5
View File
@@ -58,7 +58,7 @@ const BookerComponent = ({
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const animationScope = useBookerResizeAnimation(layout, bookerState);
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
const isMonthView = layout === BookerLayouts.MONTH_VIEW;
// We only want the initial url value, that's why we memo it. The embed seems to change the url, which sometimes drops
// the layout query param.
const layoutFromQueryParam = useMemo(() => validateLayout(getQueryParam("layout") as BookerLayouts), []);
@@ -109,21 +109,27 @@ const BookerComponent = ({
return (
<>
<div className="text-default flex h-full w-full flex-col items-center overflow-x-clip">
<div className="text-default flex min-h-full w-full flex-col items-center overflow-clip">
<div
ref={animationScope}
className={classNames(
// Sets booker size css variables for the size of all the columns.
...getBookerSizeClassNames(layout, bookerState),
"bg-default dark:bg-muted grid max-w-full auto-rows-fr items-start dark:[color-scheme:dark] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === BookerLayouts.MONTH_VIEW && "rounded-md border",
"bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === BookerLayouts.MONTH_VIEW && "border-subtle rounded-md border",
!isEmbed && "sm:transition-[width] sm:duration-300",
isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-booker sm:border-booker-width",
!isEmbed && layout === BookerLayouts.MONTH_VIEW && "border-subtle",
layout === BookerLayouts.MONTH_VIEW && isEmbed && "mt-20"
)}>
<AnimatePresence>
<BookerSection area="header" className={classNames(isMonthView && "fixed top-3 right-3 z-10")}>
<BookerSection
area="header"
className={classNames(
layout === BookerLayouts.MONTH_VIEW && "fixed top-3 right-3 z-10",
(layout === BookerLayouts.COLUMN_VIEW || layout === BookerLayouts.WEEK_VIEW) &&
"bg-muted sticky top-0 z-10"
)}>
<Header
enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={extraDays}
@@ -4,6 +4,7 @@ import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Badge, Dialog, DialogContent } from "@calcom/ui";
import { useTimePreferences } from "../../../lib";
import { useBookerStore } from "../../store";
import { useEvent } from "../../utils/event";
import { BookEventForm } from "./BookEventForm";
@@ -14,6 +15,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
const selectedDuration = useBookerStore((state) => state.selectedDuration);
const { data } = useEvent();
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
const { timeFormat } = useTimePreferences();
return (
<Dialog open={visible} onOpenChange={onCancel}>
@@ -24,7 +26,9 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
<h1 className="font-cal text-emphasis text-xl leading-5">{t("confirm_your_details")} </h1>
<div className="my-4 flex space-x-2 rounded-md leading-none">
<Badge variant="grayWithoutHover" startIcon={Calendar} size="lg">
<span>{parsedSelectedTimeslot.format("LLL")}</span>
<span>
{parsedSelectedTimeslot.format("LL")} {parsedSelectedTimeslot.format(timeFormat)}
</span>
</Badge>
{(selectedDuration || data?.length) && (
<Badge variant="grayWithoutHover" startIcon={Clock} size="lg">
@@ -4,43 +4,53 @@ import dayjs from "@calcom/dayjs";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarAvailableTimeslots } from "@calcom/features/calendars/weeklyview/types/state";
import { useTimePreferences } from "../../lib/timePreferences";
import { useBookerStore } from "../store";
import { useScheduleForEvent } from "../utils/event";
import { useEvent, useScheduleForEvent } from "../utils/event";
export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
const selectedDate = useBookerStore((state) => state.selectedDate);
const date = selectedDate || dayjs().format("YYYY-MM-DD");
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const selectedEventDuration = useBookerStore((state) => state.selectedDuration);
const schedule = useScheduleForEvent({
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
});
const { timezone } = useTimePreferences();
const event = useEvent();
const eventDuration = selectedEventDuration || event?.data?.length || 30;
const availableSlots = useMemo(() => {
const availableTimeslots: CalendarAvailableTimeslots = {};
if (!schedule.data) return availableTimeslots;
if (!schedule.data.slots) return availableTimeslots;
for (const day in schedule.data.slots) {
availableTimeslots[day] = schedule.data.slots[day].map((slot) => ({
start: dayjs(slot.time).toDate(),
end: dayjs(slot.time).add(30, "minutes").toDate(),
// First formatting to LLL and then passing it to date prevents toDate()
// from changing the timezone to users local machine (instead of itmezone selected in UI dropdown)
start: new Date(dayjs(slot.time).utc().tz(timezone).format("LLL")),
end: new Date(dayjs(slot.time).utc().tz(timezone).add(eventDuration, "minutes").format("LLL")),
}));
}
return availableTimeslots;
}, [schedule]);
}, [schedule, timezone, eventDuration]);
return (
<div className="h-full">
<div className="h-full [--calendar-dates-sticky-offset:66px]">
<Calendar
isLoading={schedule.isLoading}
availableTimeslots={availableSlots}
startHour={8}
endHour={18}
startHour={0}
endHour={23}
events={[]}
startDate={selectedDate ? new Date(selectedDate) : new Date()}
endDate={dayjs(selectedDate).add(extraDays, "day").toDate()}
onEmptyCellClick={(date) => setSelectedTimeslot(date.toString())}
gridCellsPerHour={2}
hoverEventDuration={30}
gridCellsPerHour={60 / eventDuration}
hoverEventDuration={eventDuration}
hideHeader
/>
</div>
+4 -4
View File
@@ -87,8 +87,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
week_view: {
default: {
width: "100vw",
minHeight: "450px",
height: "100vh",
minHeight: "100vh",
height: "auto",
gridTemplateAreas: `
"meta header header"
"meta main main"
@@ -100,8 +100,8 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
column_view: {
default: {
width: "100vw",
minHeight: "450px",
height: "100vh",
minHeight: "100vh",
height: "auto",
gridTemplateAreas: `
"meta header header"
"meta main main"
@@ -1,15 +1,19 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef } from "react";
import { classNames } from "@calcom/lib";
import { useCalendarStore } from "../state/store";
import "../styles/styles.css";
import type { CalendarComponentProps } from "../types/state";
import { calculateHourSizeInPx, getDaysBetweenDates, getHoursToDisplay } from "../utils";
import { getDaysBetweenDates, getHoursToDisplay } from "../utils";
import { DateValues } from "./DateValues";
import { EmptyCell } from "./event/Empty";
import { CurrentTime } from "./currentTime";
import { AvailableCellsForDay, EmptyCell } from "./event/Empty";
import { EventList } from "./event/EventList";
import { SchedulerColumns } from "./grid";
import { SchedulerHeading } from "./heading/SchedulerHeading";
import { HorizontalLines } from "./horizontalLines";
import { Spinner } from "./spinner/Spinner";
import { VeritcalLines } from "./verticalLines";
export function Calendar(props: CalendarComponentProps) {
@@ -28,26 +32,9 @@ export function Calendar(props: CalendarComponentProps) {
const hideHeader = useCalendarStore((state) => state.hideHeader);
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
const hours = useMemo(() => getHoursToDisplay(startHour || 0, endHour || 23), [startHour, endHour]);
const numberOfGridStopsPerDay = hours.length * usersCellsStopsPerHour;
const [hourSize, setHourSize] = useState(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
// Reset one hour size on window resize.
useLayoutEffect(() => {
const onResize = () => {
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
};
window.addEventListener("resize", onResize);
// By running this set function also one time in the uselayouteffect, instead of
// only in the default value of useState, we make sure that the ref is rendered
// to the screen and we read the correct value.
setHourSize(calculateHourSizeInPx(schedulerGrid?.current, startHour, endHour));
return () => window.removeEventListener("resize", onResize);
}, [startHour, endHour]);
const hourSize = 58;
// Initalise State on inital mount
useEffect(() => {
@@ -57,7 +44,7 @@ export function Calendar(props: CalendarComponentProps) {
return (
<MobileNotSupported>
<div
className="scheduler-wrapper flex h-full w-full flex-col overflow-y-scroll"
className={classNames("scheduler-wrapper flex h-full w-full flex-col")}
style={
{
"--one-minute-height": `calc(${hourSize}px/60)`,
@@ -65,6 +52,7 @@ export function Calendar(props: CalendarComponentProps) {
} as React.CSSProperties // This can't live in the css file because it's a dynamic value and css variable gets super
}>
{hideHeader !== true && <SchedulerHeading />}
{props.isLoading && <Spinner />}
<div
ref={container}
className="bg-default dark:bg-muted relative isolate flex h-full flex-auto flex-col">
@@ -72,13 +60,8 @@ export function Calendar(props: CalendarComponentProps) {
style={{ width: "165%" }}
className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
<DateValues containerNavRef={containerNav} days={days} />
{/* TODO: Implement this at a later date. */}
{/* <CurrentTime
containerNavRef={containerNav}
containerOffsetRef={containerOffset}
containerRef={container}
/> */}
<div className="flex flex-auto">
<div className="relative flex flex-auto">
<CurrentTime />
<div className="bg-default dark:bg-muted ring-muted sticky left-0 z-10 w-14 flex-none ring-1" />
<div
className="grid flex-auto grid-cols-1 grid-rows-1 [--disabled-gradient-foreground:#E6E7EB] [--disabled-gradient-background:#F8F9FB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
@@ -94,40 +77,6 @@ export function Calendar(props: CalendarComponentProps) {
/>
<VeritcalLines days={days} />
{/* Empty Cells */}
<SchedulerColumns
zIndex={50}
ref={schedulerGrid}
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
<>
{[...Array(days.length)].map((_, i) => (
<li
className="relative"
key={i}
style={{
gridRow: `2 / span ${numberOfGridStopsPerDay}`,
}}>
{/* While startDate < endDate: */}
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
const key = `${i}-${j}`;
return (
<EmptyCell
key={key}
day={days[i].toDate()}
gridCellIdx={j}
totalGridCells={numberOfGridStopsPerDay}
selectionLength={endHour - startHour}
startHour={startHour}
availableSlots={availableTimeslots}
/>
);
})}
</li>
))}
</>
</SchedulerColumns>
<SchedulerColumns
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
@@ -141,6 +90,49 @@ export function Calendar(props: CalendarComponentProps) {
);
})}
</SchedulerColumns>
{/* Empty Cells */}
<SchedulerColumns
ref={schedulerGrid}
offsetHeight={containerOffset.current?.offsetHeight}
gridStopsPerDay={numberOfGridStopsPerDay}>
<>
{[...Array(days.length)].map((_, i) => (
<li
className="relative"
key={i}
style={{
gridRow: `1 / span ${numberOfGridStopsPerDay}`,
}}>
{/* While startDate < endDate: */}
{availableTimeslots ? (
<AvailableCellsForDay
key={days[i].toISOString()}
day={days[i].toDate()}
startHour={startHour}
availableSlots={availableTimeslots}
/>
) : (
<>
{[...Array(numberOfGridStopsPerDay)].map((_, j) => {
const key = `${i}-${j}`;
return (
<EmptyCell
key={key}
day={days[i].toDate()}
gridCellIdx={j}
totalGridCells={numberOfGridStopsPerDay}
selectionLength={endHour - startHour}
startHour={startHour}
/>
);
})}
</>
)}
</li>
))}
</>
</SchedulerColumns>
</div>
</div>
</div>
@@ -12,7 +12,7 @@ export function DateValues({ days, containerNavRef }: Props) {
return (
<div
ref={containerNavRef}
className="bg-default dark:bg-muted border-b-subtle sticky top-0 z-30 flex-none border-b sm:pr-8">
className="bg-default dark:bg-muted border-b-subtle sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b sm:pr-8">
<div className="text-subtle flex text-sm leading-6 sm:hidden" data-dayslength={days.length}>
{days.map((day) => {
const isToday = dayjs().isSame(day, "day");
@@ -1,43 +1,54 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { useCalendarStore } from "../../state/store";
type Props = {
containerNavRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement>;
containerOffsetRef: React.RefObject<HTMLDivElement>;
};
export function CurrentTime({ containerOffsetRef }: Props) {
const [currentTimePos, setCurrentTimePos] = useState<number>(0);
export function CurrentTime() {
const currentTimeRef = useRef<HTMLDivElement>(null);
const [scrolledIntoView, setScrolledIntoView] = useState(false);
const [currentTimePos, setCurrentTimePos] = useState<number | null>(null);
const { startHour, endHour } = useCalendarStore((state) => ({
startHour: state.startHour || 0,
endHour: state.endHour || 23,
}));
const { timeFormat } = useTimePreferences();
useEffect(() => {
// Set the container scroll position based on the current time.
const currentHour = new Date().getHours();
let currentMinute = new Date().getHours() * 60;
currentMinute = currentMinute + new Date().getMinutes();
if (containerOffsetRef.current) {
const totalHours = endHour - startHour;
const currentTimePos = currentMinute;
setCurrentTimePos(currentTimePos);
if (currentHour > endHour || currentHour < startHour) {
setCurrentTimePos(null);
}
const minutesFromStart = currentMinute - startHour * 60;
setCurrentTimePos(minutesFromStart);
if (!currentTimeRef.current || scrolledIntoView) return;
// Within a small timeout so element has time to render.
setTimeout(() => {
currentTimeRef?.current?.scrollIntoView({ block: "center" });
setScrolledIntoView(true);
}, 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startHour, endHour]);
}, [startHour, endHour, scrolledIntoView]);
return (
<div
className="absolute z-40 ml-3 flex h-1 items-center justify-center border-gray-800"
ref={currentTimeRef}
className="absolute top-0 z-40 flex h-px items-center justify-center text-xs"
aria-hidden="true"
style={{ top: `calc(${currentTimePos}*var(--one-minute-height))`, zIndex: 100 }}>
{dayjs().format("HH:mm")}
<div className="ml-1 h-3 w-1 bg-gray-800" />
<div className="h-1 w-full bg-gray-800" />
style={{
top: `calc(${currentTimePos}*var(--one-minute-height) + var(--calendar-offset-top))`,
zIndex: 70,
}}>
<div className="w-14 pr-2 text-right">{dayjs().format(timeFormat)}</div>
<div className="bg-inverted h-3 w-px" />
<div className="bg-inverted h-px w-screen" />
</div>
);
}
@@ -1,5 +1,7 @@
import { useMemo } from "react";
import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import { classNames } from "@calcom/lib";
@@ -10,10 +12,67 @@ import type { GridCellToDateProps } from "../../utils";
import { gridCellToDateTime } from "../../utils";
type EmptyCellProps = GridCellToDateProps & {
availableSlots?: CalendarAvailableTimeslots;
isDisabled?: boolean;
topOffsetMinutes?: number;
};
export function EmptyCell(props: EmptyCellProps) {
const cellToDate = gridCellToDateTime({
day: props.day,
gridCellIdx: props.gridCellIdx,
totalGridCells: props.totalGridCells,
selectionLength: props.selectionLength,
startHour: props.startHour,
});
const minuesFromStart =
(cellToDate.toDate().getHours() - props.startHour) * 60 + cellToDate.toDate().getMinutes();
return <Cell topOffsetMinutes={minuesFromStart} timeSlot={cellToDate} />;
}
type AvailableCellProps = {
availableSlots: CalendarAvailableTimeslots;
day: GridCellToDateProps["day"];
startHour: GridCellToDateProps["startHour"];
};
export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) {
const slotsForToday = availableSlots && availableSlots[dayjs(day).format("YYYY-MM-DD")];
const slots = useMemo(
() =>
slotsForToday?.map((slot) => ({
slot,
topOffsetMinutes: (slot.start.getHours() - startHour) * 60 + slot.start.getMinutes(),
})),
[slotsForToday, startHour]
);
if (!availableSlots) return null;
return (
<>
{slots?.map((slot) => {
return (
<Cell
key={slot.slot.start.toISOString()}
timeSlot={dayjs(slot.slot.start)}
topOffsetMinutes={slot.topOffsetMinutes}
/>
);
})}
</>
);
}
type CellProps = {
isDisabled?: boolean;
topOffsetMinutes?: number;
timeSlot: Dayjs;
};
function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
const timeFormat = useTimePreferences((state) => state.timeFormat);
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
(state) => ({
@@ -23,46 +82,37 @@ export function EmptyCell(props: EmptyCellProps) {
shallow
);
const cellToDate = gridCellToDateTime({
day: props.day,
gridCellIdx: props.gridCellIdx,
totalGridCells: props.totalGridCells,
selectionLength: props.selectionLength,
startHour: props.startHour,
});
// Empty cell won't show it self (be disabled) when
// availableslots is passed in, and it's curren time is not part of the available slots
const isDisabled =
props.availableSlots &&
!props.availableSlots[dayjs(props.day).format("YYYY-MM-DD")]?.find(
(slot) => slot.start.getTime() === cellToDate.toDate().getTime()
);
return (
<div
className={classNames(
"group flex w-full items-center justify-center",
"group flex w-[calc(100%-1px)] items-center justify-center",
isDisabled && "pointer-events-none",
!isDisabled && "bg-default dark:bg-muted"
!isDisabled && "bg-default dark:bg-muted",
topOffsetMinutes && "absolute"
)}
data-disabled={isDisabled}
data-day={props.day.toISOString()}
style={{ height: `calc(${hoverEventDuration}*var(--one-minute-height))`, overflow: "visible" }}
onClick={() => onEmptyCellClick && onEmptyCellClick(cellToDate.toDate())}>
data-slot={timeSlot.toISOString()}
style={{
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
overflow: "visible",
top: topOffsetMinutes ? `calc(${topOffsetMinutes}*var(--one-minute-height))` : undefined,
}}
onClick={() => onEmptyCellClick && onEmptyCellClick(timeSlot.toDate())}>
{!isDisabled && hoverEventDuration !== 0 && (
<div
className="opacity-4 bg-subtle hover:bg-emphasis text-emphasis dark:border-emphasis absolute hidden
rounded-[4px]
border-[1px] border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
className={classNames(
"opacity-4 bg-subtle hover:bg-emphasis text-emphasis dark:border-emphasis absolute hidden rounded-[4px] border-[1px] border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:flex group-hover:cursor-pointer",
hoverEventDuration && hoverEventDuration > 15 && "items-start pt-3",
hoverEventDuration && hoverEventDuration < 15 && "items-center"
)}
style={{
height: `calc(${hoverEventDuration}*var(--one-minute-height) - 2px)`,
zIndex: 49,
zIndex: 80,
// @TODO: This used to be 90% as per Sean's work. I think this was needed when
// multiple events are stacked next to each other. We might need to add this back later.
width: "calc(100% - 2px)",
}}>
<div className=" overflow-ellipsis leading-4">{cellToDate.format(timeFormat)}</div>
<div className="overflow-ellipsis leading-[0]">{timeSlot.format(timeFormat)}</div>
</div>
)}
</div>
@@ -1,6 +1,7 @@
import { useId } from "react";
import type dayjs from "@calcom/dayjs";
import { useTimePreferences } from "@calcom/features/bookings/lib";
export const HorizontalLines = ({
hours,
@@ -11,7 +12,9 @@ export const HorizontalLines = ({
numberOfGridStopsPerCell: number;
containerOffsetRef: React.RefObject<HTMLDivElement>;
}) => {
const finalHour = hours[hours.length - 1].add(1, "hour").format("h A");
const { timeFormat } = useTimePreferences();
// We need to force the minute to zero, because otherwise in ex GMT+5.5, it would show :30 minute times (but at the positino of :00)
const finalHour = hours[hours.length - 1].add(1, "hour").minute(0).format(timeFormat);
const id = useId();
return (
<div
@@ -19,15 +22,14 @@ export const HorizontalLines = ({
style={{
gridTemplateRows: `repeat(${hours.length}, minmax(var(--gridDefaultSize),1fr)`,
}}>
<div className="row-end-1 h-7 " ref={containerOffsetRef} />
<div className="row-end-1 h-[--calendar-offset-top] " ref={containerOffsetRef} />
{hours.map((hour) => (
<>
<div key={`${id}-${hour.get("hour")}`}>
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
{hour.format("h A")}
</div>
<div key={`${id}-${hour.get("hour")}`}>
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
{/* We need to force the minute to zero, because otherwise in ex GMT+5.5, it would show :30 minute times (but at the positino of :00) */}
{hour.minute(0).format(timeFormat)}
</div>
</>
</div>
))}
<div key={`${id}-${finalHour}`}>
<div className="text-muted sticky left-0 z-20 -mt-2.5 -ml-14 w-14 pr-2 text-right text-xs leading-5">
@@ -0,0 +1,15 @@
export const Spinner = () => (
<div className="fixed left-[calc(50%+calc(var(--booker-meta-width,0px)/2))] top-1/2 z-[80] h-10 w-10 -translate-x-1/2 -translate-y-1/2">
<svg className="h-10 w-10" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
className="fill-default"
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/>
<path
className="animate-spinning fill-emphasis origin-center"
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
/>
</svg>
</div>
);
@@ -3,6 +3,7 @@
--hoursInDay: 24;
--minuteInHour:60;
--gridMobileSize: 100vw;
--calendar-offset-top: 28px;
}
.scheduler-grid-row-template{
@@ -120,6 +120,6 @@ export type CalendarState = {
hideHeader?: boolean;
};
export type CalendarComponentProps = CalendarPublicActions & CalendarState;
export type CalendarComponentProps = CalendarPublicActions & CalendarState & { isLoading?: boolean };
export type CalendarStoreProps = CalendarComponentProps & CalendarPrivateActions;