Files
calendar/packages/features/timezone-buddy/components/TimeDial.tsx
T
+16 67bb2ed798 feat: Availability slider for orgs (#10740)
* Init + get timezone + offset data agh

* Add 12/24h mode - style correctly

* User users timezone + working hours. Still some stuff to figure out

* Multiple working hours

* move calc to once per day

* Demo with two users and differnt timezones

* availabillity control tab via search params

* WIP hover overlay

* THIS WORKS ISH

* fix: multiple duration getSchedule calls [CAL-2336] (#10709)

Co-authored-by: Omar López <zomars@me.com>

* New Crowdin translations by Github Action

* fix: If the input type "Name" is selected, the label can't be changed from our default label "Your Name" (#10618)

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* New Crowdin translations by Github Action

* test: Create unit tests for react components in packages/ui/components/form/step (#10442)

Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: element call app added (#10585)

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* New Crowdin translations by Github Action

* New Crowdin translations by Github Action

* fix: e2e test for rescheduling overlapping time (#10721)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* feat: mailhog fixture (#10606)

* feat: mailhog fixture

* fix: nodemailer to dispatch emails with e2e env

* fix: remove space from email subject

* feat: fixture getFirstEventAsOwner

* feat: assert email subjects

* fix: and enable dynamic booking test (#10642)

* fix and enable dynamic booking test

* remove page pause

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* fix: Broken team events if a user with the same name exists (#10724)

* fix: Broken team events if a user with the same name exists

* Fix tests + fix usernameList optionality

* Try to list calendars, if not continue (#10720)

Co-authored-by: Omar López <zomars@me.com>

* v3.1.9

* link to org settings (#10718)

* feat: app paypal payment (#8797)

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>

* fix: RTL issues on booking pages + email confirmation (#10526)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: merge conflict

* Fixing org slug (#10538)

* fix: paypal build fixes

* Fix avatar for org in Shell top (#10712)

* feat: add range of dates for availability over-ride (#10462)

* feat: add range of dates for availability over-ride

* chore: changed range select to multiple select

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* fix: border issue for time slots (#10577)

Co-authored-by: Raghul D <v-raghuld@microsoft.com>

* style: Fix text wrapping issue in button (#10725)

Co-authored-by: Omar López <zomars@me.com>

* New Crowdin translations by Github Action

* fix: App Install Dropdown Sort Properly [CAL-2285] (#10672)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: link escaping in booking page (#10360)

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* chore: fix refund i18n message (#10731)

* chore: remove tailwind-scrollbar warning (#10523)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>

* New Crowdin translations by Github Action

* chore: Simplified date overrides (#10728)

* chore: Simplified date overrides

* Fixed a test that had a date override that wasn't at midnight utc

* Wrote test that showed a fixed Europe/Brussels

* Lint fix

* New Crowdin translations by Github Action

* Fix offset time + fetching correct dates

* Deal with awkward minute offsets

* remove store overhead

* Format H based on tz

* Cleanup store logic

* Cleanup

* Remove comments

* Remove comments

* Remove yarn.lock

* Dark mode & v-align text fixes

* Move ButtonGroup to the left to prevent chevron from jumping

* Shift based on timezone (non-hour) and have 15min granularity in hour display background color

---------

Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Zain Gulbaz <zaingulbaz8@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Suyash Srivastava <suyashsrivastava5053@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Pradumn Kumar <pradumn@tealfeed.com>
Co-authored-by: Raghul <123321540+Raghul18@users.noreply.github.com>
Co-authored-by: Raghul D <v-raghuld@microsoft.com>
Co-authored-by: ABDERRAHMANI IDRISSI HAMZA <97639117+idrissi-hamza@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: Danila <daniil.demidovich@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-18 16:32:24 +00:00

220 lines
8.1 KiB
TypeScript

import { useContext } from "react";
import { useStore } from "zustand";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import type { DateRange } from "@calcom/lib/date-ranges";
import { DAY_CELL_WIDTH } from "../constants";
import { TBContext } from "../store";
interface TimeDialProps {
timezone: string;
dateRanges?: DateRange[];
}
function isMidnight(h: number) {
return h <= 5 || h >= 22;
}
function isCurrentHourInRange({
dateRanges,
cellDate,
offset,
}: {
dateRanges?: DateRange[];
cellDate: Dayjs;
offset: number;
}): {
rangeOverlap?: number;
isInRange: boolean;
} {
if (!dateRanges)
return {
isInRange: false,
};
const currentHour = cellDate.hour();
let rangeOverlap = 0;
// yes or no answer whether it's in range.
const isFullyInRange = dateRanges.some((time) => {
if (!time) null;
const startHour = dayjs(time.start).subtract(offset, "hour");
const endHour = dayjs(time.end).subtract(offset, "hour");
// If not same day number then we don't care
if (startHour.day() !== cellDate.day()) {
return false;
}
// this is a weird way of doing this
const newDate = dayjs(time.start).set("hour", currentHour);
const diffStart = newDate.diff(startHour, "minutes");
if (Math.abs(diffStart) < 60 && diffStart != 0) {
rangeOverlap =
diffStart < 0
? -(Math.floor(startHour.minute() / 15) * 25)
: Math.floor(startHour.minute() / 15) * 25;
}
const diffEnd = newDate.diff(endHour, "minutes");
if (Math.abs(diffEnd) < 60 && diffEnd != 0) {
rangeOverlap =
diffEnd < 0 ? -(Math.floor(endHour.minute() / 15) * 25) : Math.floor(endHour.minute() / 15) * 25;
}
return newDate.isBetween(startHour, endHour, undefined, "[)"); // smiley faces or something
});
// most common situation, bail early.
if (isFullyInRange) {
return {
isInRange: isFullyInRange,
};
}
return {
isInRange: !!rangeOverlap,
rangeOverlap, // value from -75 to 75 to indicate range overlap
};
}
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 usersTimezoneDate = dayjs(browsingDate).tz(timezone);
const nowDate = dayjs(browsingDate);
const offset = (nowDate.utcOffset() - usersTimezoneDate.utcOffset()) / 60;
const hours = Array.from({ length: 24 }, (_, i) => i - offset + 1);
const days = [
hours.filter((i) => i < 0).map((i) => (i + 24) % 24),
hours.filter((i) => i >= 0 && i < 24),
hours.filter((i) => i >= 24).map((i) => i % 24),
];
let minuteOffsetApplied = false;
return (
<>
<div className="flex items-end overflow-auto text-sm">
{days.map((day, i) => {
if (!day.length) return null;
const dateWithDaySet = usersTimezoneDate.add(i - 1, "day");
return (
<div key={i} className={classNames("border-subtle overflow-hidden rounded-lg border-2")}>
<div className="flex flex-none">
{day.map((h) => {
const hours = Math.floor(h); // Whole number part
const fractionalHours = h - hours; // Decimal part
// Convert the fractional hours to minutes
const minutes = fractionalHours * 60;
const hourSet = dateWithDaySet.set("hour", h).set("minute", minutes);
const { isInRange, rangeOverlap = 0 } = isCurrentHourInRange({
dateRanges,
offset,
cellDate: hourSet,
});
const rangeGradients: {
backgroundGradient?: string;
textGradient?: string;
darkTextGradient?: string;
} = {};
if (isInRange && rangeOverlap) {
if (rangeOverlap < 0) {
const gradientValue = Math.abs(rangeOverlap);
rangeGradients.backgroundGradient = `linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) ${gradientValue}%, var(--cal-bg-success) ${gradientValue}%)`;
rangeGradients.textGradient = `linear-gradient(90deg, rgba(0,212,255,1) 0%, rgba(2,0,36,1) 100%, rgba(9,108,121,1) 100%)`;
rangeGradients.darkTextGradient = `linear-gradient(90deg, var(--cal-text-emphasis, #111827) ${
gradientValue === 50 ? "50%" : Math.round(gradientValue / 100) * 100 + "%"
}, var(--cal-text-inverted, white) 0%, var(--cal-text-inverted, white) 0%)`;
} else {
rangeGradients.backgroundGradient = `linear-gradient(90deg, var(--cal-bg-success) ${rangeOverlap}%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%)`;
rangeGradients.textGradient = `linear-gradient(90deg, rgba(0,212,255,1) 0%, rgba(2,0,36,1) 50%, rgba(9,108,121,1) 100%)`;
rangeGradients.darkTextGradient = `linear-gradient(90deg, var(--cal-text-inverted, white) ${
rangeOverlap === 50 ? "50%" : Math.round(rangeOverlap / 100) * 100 + "%"
}, var(--cal-text-emphasis, #111827) 0%, var(--cal-text-emphasis, #111827) 0%)`;
}
}
const minuteOffsetStyles: { marginLeft?: string } = {};
if (hours !== 0 && !minuteOffsetApplied) {
minuteOffsetApplied = true;
minuteOffsetStyles.marginLeft = `${DAY_CELL_WIDTH * (offset % 1)}px`;
}
return (
<div
key={h}
className={classNames(
"flex h-8 flex-col items-center justify-center",
isInRange ? "text-emphasis dark:text-inverted" : "",
isInRange && !rangeOverlap ? "bg-success" : "",
hours ? "" : "bg-subtle font-medium"
)}
style={{
...minuteOffsetStyles,
width: `${DAY_CELL_WIDTH}px`,
backgroundImage: rangeGradients.backgroundGradient,
}}>
{hours ? (
<div title={hourSet.format("DD/MM HH:mm")}>
<div className="flex flex-col text-center text-xs leading-3">
{rangeGradients.textGradient ? (
<>
{/* light mode */}
<span className={classNames("text-1xl font-bold dark:hidden")}>
{hourSet.format("H")}
</span>
{/* dark mode */}
<span
style={{
backgroundImage: rangeGradients.darkTextGradient,
}}
className={classNames(
"text-1xl hidden font-bold dark:block",
rangeOverlap ? "bg-clip-text text-transparent" : ""
)}>
{hourSet.format("H")}
</span>
</>
) : (
<span className="text-1xl font-bold">{hourSet.format("H")}</span>
)}
</div>
</div>
) : (
<div className="flex flex-col text-center text-xs leading-3">
<span>{hourSet.format("MMM")}</span>
<span>{hourSet.format("DD")}</span>
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
</>
);
}