diff --git a/apps/storybook/stories/ToggleGroup.stories.tsx b/apps/storybook/stories/ToggleGroup.stories.tsx new file mode 100644 index 0000000000..212841aae6 --- /dev/null +++ b/apps/storybook/stories/ToggleGroup.stories.tsx @@ -0,0 +1,20 @@ +import { ComponentMeta } from "@storybook/react"; + +import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup"; + +export default { + title: "Toggle Group", + component: ToggleGroup, +} as ComponentMeta; + +export const Default = () => { + return ( + + ); +}; diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index 43db34e51b..3ecf9fd0e4 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -4,15 +4,18 @@ import { FC, useEffect, useState } from "react"; import dayjs, { Dayjs } from "@calcom/dayjs"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { TimeFormat } from "@calcom/lib/timeFormat"; import { nameOfDay } from "@calcom/lib/weekday"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots"; import { SkeletonContainer, SkeletonText } from "@calcom/ui"; +import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup"; import classNames from "@lib/classNames"; import { timeZone } from "@lib/clock"; type AvailableTimesProps = { - timeFormat: string; + timeFormat: TimeFormat; + onTimeFormatChange: (is24Hour: boolean) => void; eventTypeId: number; recurringCount: number | undefined; eventTypeSlug: string; @@ -31,6 +34,7 @@ const AvailableTimes: FC = ({ eventTypeSlug, recurringCount, timeFormat, + onTimeFormatChange, seatsPerTimeSlot, ethSignature, }) => { @@ -48,14 +52,25 @@ const AvailableTimes: FC = ({ return (
-
- - {nameOfDay(i18n.language, Number(date.format("d")))} - - - {date.format(", D ")} - {date.toDate().toLocaleString(i18n.language, { month: "long" })} - +
+
+ + {nameOfDay(i18n.language, Number(date.format("d")), "short")} + + + , {date.toDate().toLocaleString(i18n.language, { month: "long", day: "numeric" })} + +
+
+ onTimeFormatChange(timeFormat === "24")} + defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"} + options={[ + { value: "12", label: t("12_hour_short") }, + { value: "24", label: t("24_hour_short") }, + ]} + /> +
{slots.length > 0 && diff --git a/apps/web/components/booking/TimeOptions.tsx b/apps/web/components/booking/TimeOptions.tsx index 92d9e33902..d62b3ff2c0 100644 --- a/apps/web/components/booking/TimeOptions.tsx +++ b/apps/web/components/booking/TimeOptions.tsx @@ -1,25 +1,19 @@ import { FC, useEffect, useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Switch } from "@calcom/ui/v2"; import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect"; -import { is24h, timeZone } from "../../lib/clock"; +import { timeZone } from "../../lib/clock"; type Props = { onSelectTimeZone: (selectedTimeZone: string) => void; - onToggle24hClock: (is24hClock: boolean) => void; - timeFormat: string; - hideTimeFormatToggle?: boolean; }; -const TimeOptions: FC = ({ onToggle24hClock, onSelectTimeZone, timeFormat, hideTimeFormatToggle }) => { +const TimeOptions: FC = ({ onSelectTimeZone }) => { const [selectedTimeZone, setSelectedTimeZone] = useState(""); - const [is24hClock, setIs24hClock] = useState(timeFormat === "HH:mm" && true); const { t } = useLocale(); useEffect(() => { - setIs24hClock(is24h()); setSelectedTimeZone(timeZone()); }, []); @@ -29,28 +23,10 @@ const TimeOptions: FC = ({ onToggle24hClock, onSelectTimeZone, timeFormat } }, [selectedTimeZone, onSelectTimeZone]); - const handle24hClockToggle = (is24hClock: boolean) => { - setIs24hClock(is24hClock); - onToggle24hClock(is24h(is24hClock)); - }; - return selectedTimeZone !== "" ? (
{t("time_options")}
- {!hideTimeFormatToggle && ( -
- - -
- )}
{ const router = useRouter(); const path = router.asPath.split("/"); @@ -109,6 +108,7 @@ const useSlots = ({ const SlotPicker = ({ eventType, timeFormat, + onTimeFormatChange, timeZone, recurringEventCount, users, @@ -117,7 +117,8 @@ const SlotPicker = ({ ethSignature, }: { eventType: Pick; - timeFormat: string; + timeFormat: TimeFormat; + onTimeFormatChange: (is24Hour: boolean) => void; timeZone?: string; seatsPerTimeSlot?: number; recurringEventCount?: number; @@ -201,6 +202,7 @@ const SlotPicker = ({ slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]} date={selectedDate} timeFormat={timeFormat} + onTimeFormatChange={onTimeFormatChange} eventTypeId={eventType.id} eventTypeSlug={eventType.slug} seatsPerTimeSlot={seatsPerTimeSlot} @@ -213,35 +215,20 @@ const SlotPicker = ({ }; function TimezoneDropdown({ - onChangeTimeFormat, onChangeTimeZone, timeZone, - timeFormat, - hideTimeFormatToggle, }: { - onChangeTimeFormat: (newTimeFormat: string) => void; onChangeTimeZone: (newTimeZone: string) => void; timeZone?: string; - timeFormat: string; - hideTimeFormatToggle?: boolean; }) { const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false); - useEffect(() => { - handleToggle24hClock(!!getIs24hClockFromLocalStorage()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const handleSelectTimeZone = (newTimeZone: string) => { onChangeTimeZone(newTimeZone); localStorageTimeZone(newTimeZone); setIsTimeOptionsOpen(false); }; - const handleToggle24hClock = (is24hClock: boolean) => { - onChangeTimeFormat(is24hClock ? "HH:mm" : "h:mma"); - }; - return ( @@ -260,12 +247,7 @@ function TimezoneDropdown({ hideWhenDetached align="start" className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]"> - + @@ -297,14 +279,7 @@ const useRouterQuery = (name: T) => { export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps; -const timeFormatTotimeFormatString = (timeFormat?: number | null) => { - if (!timeFormat) return null; - return timeFormat === 24 ? "HH:mm" : "h:mma"; -}; - const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { - const { data: user } = trpc.useQuery(["viewer.me"]); - const timeFormatFromProfile = timeFormatTotimeFormatString(user?.timeFormat); const router = useRouter(); const isEmbed = useIsEmbed(restProps.isEmbed); const query = dateQuerySchema.parse(router.query); @@ -317,7 +292,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { const isBackgroundTransparent = useIsBackgroundTransparent(); const [timeZone, setTimeZone] = useState(); - const [timeFormat, setTimeFormat] = useState("HH:mm"); + const [timeFormat, setTimeFormat] = useState(detectBrowserTimeFormat); + + const onTimeFormatChange = (is24Hours: boolean) => { + setTimeFormat(is24Hours ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR); + setIs24hClockInLocalStorage(is24Hours); + }; const [gateState, gateDispatcher] = useReducer( (state: GateState, newState: Partial) => ({ @@ -329,8 +309,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { useEffect(() => { setTimeZone(localStorageTimeZone() || dayjs.tz.guess()); - setTimeFormat(timeFormatFromProfile || detectBrowserTimeFormat); - }, [timeFormatFromProfile]); + }, []); // TODO: Improve this; useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO"); @@ -352,18 +331,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : []; const timezoneDropdown = useMemo( - () => ( - - ), - [timeZone, timeFormat, timeFormatFromProfile] + () => , + [timeZone] ); const stripeAppData = getStripeAppData(eventType); const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {}; @@ -421,7 +390,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
@@ -501,6 +470,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => { } eventType={eventType} timeFormat={timeFormat} + onTimeFormatChange={onTimeFormatChange} timeZone={timeZone} users={userList} seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined} diff --git a/apps/web/pages/settings/my-account/general.tsx b/apps/web/pages/settings/my-account/general.tsx index 47ced1ea6f..6ae90a6d7f 100644 --- a/apps/web/pages/settings/my-account/general.tsx +++ b/apps/web/pages/settings/my-account/general.tsx @@ -3,7 +3,6 @@ import { useMemo } from "react"; import { useForm, Controller } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { setIs24hClockInLocalStorage } from "@calcom/lib/timeFormat"; import { inferQueryOutput, trpc } from "@calcom/trpc/react"; import { Button } from "@calcom/ui/v2/core/Button"; import Meta from "@calcom/ui/v2/core/Meta"; @@ -116,8 +115,6 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
{ - setIs24hClockInLocalStorage(values.timeFormat.value === 24); - mutation.mutate({ ...values, locale: values.locale.value, @@ -178,6 +175,9 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => { )} /> +
+ {t("timeformat_profile_hint")} +
localStorage.setItem(is24hLocalstorageKey, is24h.toString()); @@ -41,4 +46,6 @@ export const isBrowserLocale24h = () => { } }; -export const detectBrowserTimeFormat = isBrowserLocale24h() ? "HH:mm" : "h:mma"; +export const detectBrowserTimeFormat = isBrowserLocale24h() + ? TimeFormat.TWENTY_FOUR_HOUR + : TimeFormat.TWELVE_HOUR; diff --git a/packages/ui/v2/core/form/ToggleGroup.tsx b/packages/ui/v2/core/form/ToggleGroup.tsx new file mode 100644 index 0000000000..3efe040e6b --- /dev/null +++ b/packages/ui/v2/core/form/ToggleGroup.tsx @@ -0,0 +1,53 @@ +import * as RadixToggleGroup from "@radix-ui/react-toggle-group"; +import { useEffect, useState } from "react"; + +import { classNames } from "@calcom/lib"; + +export const ToggleGroupItem = () =>
hi
; + +interface ToggleGroupProps extends Omit { + options: { value: string; label: string }[]; +} + +export const ToggleGroup = ({ options, onValueChange, ...props }: ToggleGroupProps) => { + const [value, setValue] = useState(props.defaultValue); + const [activeToggleElement, setActiveToggleElement] = useState(null); + + useEffect(() => { + if (value && onValueChange) onValueChange(value); + }, [value, onValueChange]); + + return ( + <> + + {/* Active toggle. It's a separate element so we can animate it nicely. */} + + {options.map((option) => ( + { + if (node && value === option.value) { + setActiveToggleElement(node); + } + return node; + }}> + {option.label} + + ))} + + + ); +};