diff --git a/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx b/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx index b032ec126d..1e0e6fffa8 100644 --- a/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx +++ b/apps/web/modules/users/views/users-type-public-view.getServerSideProps.tsx @@ -145,7 +145,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { length: eventData.length, metadata: { ...eventData.metadata, - multipleDuration: [15, 30, 60], + multipleDuration: [15, 30, 45, 60, 90], }, }, user: usernames.join("+"), diff --git a/apps/web/playwright/dynamic-booking-pages.e2e.ts b/apps/web/playwright/dynamic-booking-pages.e2e.ts index 34317257cd..28de5a4a93 100644 --- a/apps/web/playwright/dynamic-booking-pages.e2e.ts +++ b/apps/web/playwright/dynamic-booking-pages.e2e.ts @@ -72,23 +72,23 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) = await page.waitForLoadState("networkidle"); - const badgeByDurationTestId = (duration: number) => `multiple-choice-${duration}mins`; + const listItemByDurationTestId = (duration: number) => `multiple-choice-${duration}mins`; - let badgeLocator = await page.getByTestId(badgeByDurationTestId(duration)); - let activeState = await badgeLocator.getAttribute("data-active"); + let listItemLocator = await page.getByTestId(listItemByDurationTestId(duration)); + let activeState = await listItemLocator.getAttribute("data-active"); expect(activeState).toEqual("true"); duration = 30; await page.goto(`/${pro.username}+${free.username}?duration=${duration}`); - badgeLocator = await page.getByTestId(badgeByDurationTestId(duration)); - activeState = await badgeLocator.getAttribute("data-active"); + listItemLocator = await page.getByTestId(listItemByDurationTestId(duration)); + activeState = await listItemLocator.getAttribute("data-active"); expect(activeState).toEqual("true"); // Check another badge just to ensure its not selected - badgeLocator = await page.getByTestId(badgeByDurationTestId(15)); - activeState = await badgeLocator.getAttribute("data-active"); + listItemLocator = await page.getByTestId(listItemByDurationTestId(15)); + activeState = await listItemLocator.getAttribute("data-active"); expect(activeState).toEqual("false"); }); // eslint-disable-next-line playwright/no-skipped-test diff --git a/apps/web/playwright/event-types.e2e.ts b/apps/web/playwright/event-types.e2e.ts index 621cfcf05a..905618cabd 100644 --- a/apps/web/playwright/event-types.e2e.ts +++ b/apps/web/playwright/event-types.e2e.ts @@ -64,6 +64,7 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => { const nonce = randomString(3); const eventTitle = `my recurring event ${nonce}`; await createNewEventType(page, { eventTitle }); + await page.waitForLoadState("networkidle"); await page.click("[data-testid=vertical-tab-recurring]"); await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeHidden(); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b30e514580..cf77ef8492 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -713,6 +713,7 @@ "default_duration_no_options": "Please choose available durations first", "multiple_duration_mins": "{{count}} $t(minute_timeUnit)", "multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)", + "multiple_duration_timeUnit_short": "{{count}}$t({{unit}}_short)", "minutes": "Minutes", "use_cal_ai_to_make_call_description": "Use Cal.ai to get an AI powered phone number or make calls to guests.", "round_robin": "Round Robin", @@ -1224,6 +1225,8 @@ "day_timeUnit": "days", "hour_timeUnit": "hours", "minute_timeUnit": "mins", + "minute_short": "m", + "hour_short": "h", "new_workflow_heading": "Create your first workflow", "new_workflow_description": "Workflows enable you to automate sending reminders and notifications.", "active_on": "Active on", @@ -1277,8 +1280,10 @@ "reminder_email": "Reminder: {{eventType}} with {{name}} at {{date}}", "not_triggering_existing_bookings": "Won't trigger for already existing bookings as user will be asked for phone number when booking the event.", "minute_one": "{{count}} minute", + "minute_one_short": "{{count}}m", "minute_other": "{{count}} minutes", "hour_one": "{{count}} hour", + "hour_one_short": "{{count}}h", "hour_other": "{{count}} hours", "invalid_input": "Invalid input", "broken_video_action": "We could not add the <1>{{location}} meeting link to your scheduled event. Contact your invitees or update your calendar event to add the details. You can either <3> change your location on the event type or try <5>removing and adding the app again.", diff --git a/packages/features/bookings/components/event-meta/Details.tsx b/packages/features/bookings/components/event-meta/Details.tsx index f59408c552..a3d5eeed44 100644 --- a/packages/features/bookings/components/event-meta/Details.tsx +++ b/packages/features/bookings/components/event-meta/Details.tsx @@ -142,7 +142,7 @@ export const EventDetails = ({ event, blocks = defaultEventDetailsBlocks }: Even switch (block) { case EventDetailBlocks.DURATION: return ( - + ); diff --git a/packages/features/bookings/components/event-meta/Duration.tsx b/packages/features/bookings/components/event-meta/Duration.tsx index dbd3cdc545..a897893e5a 100644 --- a/packages/features/bookings/components/event-meta/Duration.tsx +++ b/packages/features/bookings/components/event-meta/Duration.tsx @@ -1,12 +1,12 @@ import type { TFunction } from "next-i18next"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useIsPlatform } from "@calcom/atoms/monorepo"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type { BookerEvent } from "@calcom/features/bookings/types"; -import classNames from "@calcom/lib/classNames"; +import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Badge } from "@calcom/ui"; +import { useShouldShowArrows, Icon } from "@calcom/ui"; /** Render X mins as X hours or X hours Y mins instead of in minutes once >= 60 minutes */ export const getDurationFormatted = (mins: number | undefined, t: TFunction) => { @@ -19,16 +19,16 @@ export const getDurationFormatted = (mins: number | undefined, t: TFunction) => if (mins > 0) { minStr = mins === 1 - ? t("minute_one", { count: 1 }) - : t("multiple_duration_timeUnit", { count: mins, unit: "minute" }); + ? t("minute_one_short", { count: 1 }) + : t("multiple_duration_timeUnit_short", { count: mins, unit: "minute" }); } // format hours string let hourStr = ""; if (hours > 0) { hourStr = hours === 1 - ? t("hour_one", { count: 1 }) - : t("multiple_duration_timeUnit", { count: hours, unit: "hour" }); + ? t("hour_one_short", { count: 1 }) + : t("multiple_duration_timeUnit_short", { count: hours, unit: "hour" }); } if (hourStr && minStr) return `${hourStr} ${minStr}`; @@ -41,6 +41,7 @@ export const EventDuration = ({ event: Pick; }) => { const { t } = useLocale(); + const itemRefs = useRef<(HTMLLIElement | null)[]>([]); const isPlatform = useIsPlatform(); const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [ state.selectedDuration, @@ -48,6 +49,20 @@ export const EventDuration = ({ state.state, ]); + const { ref, calculateScroll, leftVisible, rightVisible } = useShouldShowArrows(); + + const handleLeft = () => { + if (ref.current) { + ref.current.scrollLeft -= 100; + } + }; + + const handleRight = () => { + if (ref.current) { + ref.current.scrollLeft += 100; + } + }; + const isDynamicEvent = "isDynamic" in event && event.isDynamic; // Sets initial value of selected duration to the default duration. @@ -57,27 +72,63 @@ export const EventDuration = ({ setSelectedDuration(event.length); }, [selectedDuration, setSelectedDuration, event.metadata?.multipleDuration, event.length, isDynamicEvent]); + useEffect(() => { + const timeout = setTimeout(() => { + if (selectedDuration && itemRefs.current[selectedDuration]) { + itemRefs.current[selectedDuration]?.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "center", + }); + } + }, 100); + return () => clearTimeout(timeout); + }, [selectedDuration]); + if ((!event?.metadata?.multipleDuration && !isDynamicEvent) || isPlatform) return <>{getDurationFormatted(event.length, t)}; const durations = event?.metadata?.multipleDuration || [15, 30, 60, 90]; - return ( -
- {durations - .filter((dur) => state !== "booking" || dur === selectedDuration) - .map((duration) => ( - setSelectedDuration(duration)}> - {getDurationFormatted(duration, t)} - - ))} + return selectedDuration ? ( +
+ {leftVisible && ( + + )} +
    calculateScroll(e)} + ref={ref}> + {durations + .filter((dur) => state !== "booking" || dur === selectedDuration) + .map((duration, index) => ( +
  • setSelectedDuration(duration)} + ref={(el) => (itemRefs.current[duration] = el)} + className={classNames( + selectedDuration === duration ? "bg-emphasis" : "hover:text-emphasis", + "text-default cursor-pointer rounded-[4px] px-3 py-1.5 text-sm leading-tight transition" + )}> +
    {getDurationFormatted(duration, t)}
    +
  • + ))} +
+ {rightVisible && ( + + )}
- ); + ) : null; }; diff --git a/packages/ui/components/apps/AllApps.tsx b/packages/ui/components/apps/AllApps.tsx index 4e3a8befe5..aac42617e5 100644 --- a/packages/ui/components/apps/AllApps.tsx +++ b/packages/ui/components/apps/AllApps.tsx @@ -26,8 +26,13 @@ export function useShouldShowArrows() { useEffect(() => { const appCategoryList = ref.current; - if (appCategoryList && appCategoryList.scrollWidth > appCategoryList.clientWidth) { - setShowArrowScroll({ left: false, right: true }); + if (appCategoryList) { + const isAtStart = appCategoryList.scrollLeft <= 0; + const isAtEnd = appCategoryList.scrollWidth <= appCategoryList.clientWidth + appCategoryList.scrollLeft; + setShowArrowScroll({ + left: !isAtStart, + right: !isAtEnd, + }); } }, []);