From 6d02ac672947e914233828cf9333c9789a03c330 Mon Sep 17 00:00:00 2001 From: Jeroen Reumkens Date: Mon, 24 Apr 2023 16:32:30 +0200 Subject: [PATCH] New Booker Component (preparations for booker atom) (#6792) * Wip on booker atom * Wip on booker atom * Added correct icon imports * Fixed build * Responsive improvements * Removed package lock * Responsive tweaks * Animation improvements and cleanup * Animation improvements and event meta layout improvements. * Tweaked margins. * Added more event meta blocks * Layout tweaks * Converted booker layout to css grid and implemented multiple layout options * cleanup * Fixed build * Fixed build * Added temporary api route to enable/disable new booker * Added sticky behavior * Reverted yarn.lock and reinstalled new packages to see if this fixes build on vercel. * Ensure divider lines always have 100% height. * Improved animation config + initial load * Ensure to pass eventid to getschedule, otherwise custom availability schedule wont work and wont return any availability * Fixed divider line heights in booker * Fixed timezone select positioning * Added ability to view multiple days of timeslots * Added icons to booker toggle * Always show timeslots in timeslots view, also if no date is selected yet. In that case we show upcoming 5 days. * Fixed timeslots in small calendar view * Show selected day in calendar * Fixed booker timeslots view * Wip in making booking form work * Moved most of the booker atom stuff to features, since it belongs there. Atom should be a rather small wrapper. * Added create event functionality to booker form. * Added guests toggle to booker form and styled input addons in dark mode. * Added dynamic weekstart to booker * Added seats limit feature to timeslots. * Removed todo * Added correct event avatars * Added correct event name and icons * Added correct translation for minutes text in multi duration * Add rescheduling functionality to new booker. * Added selected booking time to booking meta in sidebar. * Abstracted away timeformat to custom hook * Added correct key props to all components in booker. * Fix build * Create some new custom hooks to have a lot less repitition in code. * Moved bookerform component inside booker directory since it is tied to it. * Added error messages to booker form, plus fixed bug in recurring events. * Added some comments <3 * Fixed todos in booker form. * Added loading state for timeslot selector, and added prefetching of next month, in case of multi day view showing 2 months at the same time. * Fixed import paths * Added away view * Validate uniqueness of event attendees. * Tweaked comment * #5798 added correct date format and style for selected date in booker. * UI improvements * Enable possibility to add booking values via query params. * Added functionality to update query params when user selects date/duration etc in booker * First steps in adding e2e test. * Fixes after merge with main, and added new form builder. * Implemented new form types and validation to booker, confirming new form builder. Validation still throwing wrong error keys though. * Added search to timezone dropdown * Added e2e test for booker (copy of current booker tests, only enabling cookie), plus fixed reschedule view. * Updated yarn.lock * Added new booker for team pages. * Fixed input addon (hover) styles. * Added dynamic booking. * Hide timeformat select for multi day view for now. * Cleanup and ui tweaks * removed log * Mobile improvements * Cleanup * Small design tweaks after talking to ciaran. * Text color and weight tweaks in booker * Added rainbow gates to new booker. * Added in default values which fixes form vallidation (???). * Added empty defaults for name and email * Added metadata * Reset yarn.lock * Fixed booker zod validation after change in main. * Icon tweak * Fixed timezone select styles after new classnames have been merged. * Updated seat availability styles. * Update yarn.lock * Added explanation for alchemy key to .env.example * Added tooltip to booker month/week/multiday toggle * Fixed timezoneselect styles in booker after select updates. * Updates bookingfields component by taking changes from current booker component * Removed remaining booker todos * Fix bookeventform * Fix for recurring event meta * Type fixes * Typefixes * Team event fixes * Avoid hydration errors by only rendering date picker client side. Remove web3 gates since we dont offer them anymore. Prevent timeslot select from staying open when switching to a different month. * Don't show calendar on mobile booker during booking. * Always align booker buttons to bottom * Don't show backend messages in error, rather show a helpful text like the current booker does as well. * Do invisible next rewrite based on cookie from next.config.js (#7949) * Do invisible next rewrite based on cookie from next.config.js * Name embed link instead of bookerPath * Rewrites only dynamic user pages --------- Co-authored-by: zomars * Don't allow change of timezone when bookerform is visible * Don't add duration to query param if the event is not a multi duration event. * Update next.config.js * Added correct timezone formatting to event meta when timeslot is selected. * removed .env variable that isn't needed anymore. * Update Gates.tsx * Type fixes * Allows to run all tests with the new booker * Fixed timezone select styles after merge. * Don't throw error when event doesn't have hosts, rather return no users, which will result in no availability in UI. * Make booker errors of severity info instead of warning. * Ensure team avatars are shown, as well as filter on uniqueness of avatars. * Added all booked today message to timeslots. * Added cal.com logo to booker. * Fixed fragment classname error, minor mobile animation tweaks plus make all booked today text smaller for multi day layout. * Improved timezone select styles, and updated arguments of getbooking function after updates in main. * Prevent infinite loop in rewriting new booker. * Prevent infinite loop in rewriting new booker. * Moved new-booker pages to their own directory to prevent regexes confusing next and thus nut running getserversideprops after rewrite. Also adding clearing of old date in booker store, that could stick around when user immediately navigates back to the same page after booking. * Fixed cal logo color in darkmode for new booker. * Implemented new color tokens and theme variables. Also small design tweaks after merge with main. * Minor style tweaks * Show multiple locations in tooltip on booker #8222 * Radio button style tweaks * Fixed build * Updated calendar imports to new lucide names * Removed resetting of selected times logic, because otherwise url params wouldnt be taken into account which is actually what we want. So old values sticking around when navigating back is actually the desired behavior. * Updated tests to instead of always run the new booker in tests, have a utility to run both the new and old booker for specified tests. * Added comment and eslint disable for if statement in booker test. * Update packages/features/bookings/components/event-meta/Details.tsx Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Fix badge types * Lazy loaded timezone select to save 85kb in bundle size. * Upgraded framer to latest. Als moved framer and react sticky deps to features instead of atoms. * Added new pagewrapper logic * Simplified rescheduling ssr fetches, this now also supports multi seat rescheduling. * Unset selected time when user is rescheduling directly after a new booking, otherwise it would show the form instead of new time selection. * Updated form builder logic as per form builder in current booker. * Updated form builder prefill logic as per logic in current booker. * Updated getbooking function to fetch correct details when a reschedule uid is used * Fixed booking questions test by NOT waiting for /book page because the new booker doesnt have this. * Added former meeting time to reschedule view. * Fixed types * Undo playwright config update by mistake. * Fixed event types test by only waiting for /book page in old booker * Set new booker cookie to one year in the future instead of 2050 * added reset mockdate to test * Temporary disabled test to see if this solves the out of memory error. * Deleted test to see if that fixes the memory error * Select first day when switching months in booker --------- Co-authored-by: zomars Co-authored-by: Alex van Andel Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: Sean Brydon Co-authored-by: Peer Richelsen --- .env.example | 2 +- apps/storybook/.storybook/main.js | 2 + .../components/booking/pages/BookingPage.tsx | 14 +- apps/web/components/error/error-page.tsx | 2 +- .../lib/mutations/bookings/create-booking.ts | 13 - apps/web/next.config.js | 23 + apps/web/pages/[user]/book.tsx | 4 +- apps/web/pages/_error.tsx | 3 +- apps/web/pages/api/auth/oidc.ts | 3 +- apps/web/pages/api/auth/saml/authorize.ts | 3 +- apps/web/pages/api/integrations/[...args].ts | 3 +- apps/web/pages/api/newbooker/[status].tsx | 26 + apps/web/pages/availability/[schedule].tsx | 3 +- apps/web/pages/availability/index.tsx | 2 +- apps/web/pages/booking/[uid].tsx | 2 +- apps/web/pages/d/[link]/[slug].tsx | 2 +- apps/web/pages/event-types/index.tsx | 2 +- apps/web/pages/new-booker/[user]/[type].tsx | 112 ++ .../pages/new-booker/team/[slug]/[type].tsx | 65 + apps/web/pages/team/[slug]/[type].tsx | 4 +- apps/web/pages/team/[slug]/book.tsx | 4 +- apps/web/playwright/booking-pages.e2e.ts | 23 +- apps/web/playwright/booking-seats.e2e.ts | 23 +- apps/web/playwright/event-types.e2e.ts | 19 +- apps/web/playwright/lib/fixtures.ts | 2 + apps/web/playwright/lib/new-booker.ts | 31 + .../manage-booking-questions.e2e.ts | 56 +- apps/web/playwright/reschedule.e2e.ts | 3 +- apps/web/public/static/locales/en/common.json | 9 + apps/web/test/lib/parseZone.test.ts | 2 +- jest.config.ts | 19 + packages/atoms/booker/Booker.tsx | 13 + packages/atoms/booker/booker.stories.mdx | 14 + packages/atoms/booker/export.ts | 5 + packages/atoms/booker/index.ts | 1 + packages/atoms/build.mjs | 31 + packages/atoms/globals.css | 10 + packages/atoms/index.ts | 1 + packages/atoms/package.json | 22 + packages/atoms/postcss.config.js | 8 + packages/atoms/tailwind.config.cjs | 7 + packages/atoms/tsconfig.json | 12 + packages/atoms/types.ts | 7 + packages/atoms/vite.config.ts | 28 + packages/config/package.json | 1 + packages/config/tailwind-preset.js | 14 +- packages/features/bookings/Booker/Booker.tsx | 213 +++ .../Booker/components/AvailableTimeSlots.tsx | 78 + .../bookings/Booker/components/Away.tsx | 20 + .../BookEventForm/BookEventForm.tsx | 342 ++++ .../BookEventForm/BookingFields.tsx | 119 ++ .../components/BookEventForm/Skeleton.tsx | 29 + .../Booker/components/BookEventForm/index.ts | 1 + .../bookings/Booker/components/DatePicker.tsx | 54 + .../bookings/Booker/components/EventMeta.tsx | 93 ++ .../Booker/components/LargeCalendar.tsx | 30 + .../bookings/Booker/components/Section.tsx | 62 + packages/features/bookings/Booker/config.ts | 82 + packages/features/bookings/Booker/index.ts | 2 + packages/features/bookings/Booker/store.ts | 168 ++ packages/features/bookings/Booker/types.ts | 46 + .../features/bookings/Booker/utils/dates.ts | 18 + .../features/bookings/Booker/utils/event.ts | 53 + .../bookings/Booker/utils/query-param.ts | 13 + .../bookings/components/AvailableTimes.tsx | 104 ++ .../bookings/components/TimeFormatToggle.tsx | 25 + .../components/event-meta/Details.tsx | 168 ++ .../components/event-meta/Duration.tsx | 37 + .../event-meta/EventMeta.stories.mdx | 42 + .../components/event-meta/Locations.tsx | 42 + .../components/event-meta/Members.tsx | 42 + .../components/event-meta/Occurences.tsx | 42 + .../bookings/components/event-meta/Price.tsx | 18 + .../components/event-meta/Skeleton.tsx | 19 + .../bookings/components/event-meta/Title.tsx | 15 + .../components/event-meta/event.mock.ts | 14 + .../bookings/components/event-meta/index.ts | 4 + packages/features/bookings/index.ts | 8 + .../booking-to-mutation-input-mapper.tsx | 83 + .../features/bookings/lib/create-booking.ts | 8 + .../bookings/lib}/create-recurring-booking.ts | 20 +- packages/features/bookings/lib/get-booking.ts | 163 ++ packages/features/bookings/lib/index.ts | 7 + .../features/bookings/lib/timePreferences.ts | 34 + packages/features/bookings/types.ts | 33 + .../components/CheckedUserSelect.tsx | 8 +- .../features/eventtypes/lib/getPublicEvent.ts | 201 +++ packages/features/package.json | 8 +- packages/features/schedules/index.ts | 2 + .../schedules/lib/use-schedule/index.ts | 4 + .../schedules/lib/use-schedule/types.ts | 3 + .../use-schedule/useNonEmptyScheduleDays.ts | 14 + .../schedules/lib/use-schedule/useSchedule.ts | 49 + .../lib/use-schedule/useSlotsForDate.ts | 31 + packages/features/tsconfig.json | 3 +- packages/lib/array.ts | 1 + packages/lib/date-fns/index.ts | 15 + .../http => packages/lib}/fetch-wrapper.ts | 2 +- .../lib/parse-dates.ts | 28 +- .../lib/parse-zone.ts | 0 packages/trpc/server/routers/viewer.tsx | 12 + packages/ui/components/badge/Badge.tsx | 39 +- packages/ui/components/form/index.ts | 2 +- packages/ui/components/form/inputs/Input.tsx | 15 +- .../form/timezone-select/TimezoneSelect.tsx | 40 +- .../form/toggleGroup/ToggleGroup.tsx | 62 +- .../ui/components/form/toggleGroup/index.ts | 2 +- packages/ui/components/tooltip/Tooltip.tsx | 10 +- packages/ui/index.tsx | 1 - yarn.lock | 1464 +++++++++++++++-- 110 files changed, 4723 insertions(+), 299 deletions(-) delete mode 100644 apps/web/lib/mutations/bookings/create-booking.ts create mode 100644 apps/web/pages/api/newbooker/[status].tsx create mode 100644 apps/web/pages/new-booker/[user]/[type].tsx create mode 100644 apps/web/pages/new-booker/team/[slug]/[type].tsx create mode 100644 apps/web/playwright/lib/new-booker.ts create mode 100644 packages/atoms/booker/Booker.tsx create mode 100644 packages/atoms/booker/booker.stories.mdx create mode 100644 packages/atoms/booker/export.ts create mode 100644 packages/atoms/booker/index.ts create mode 100644 packages/atoms/build.mjs create mode 100644 packages/atoms/globals.css create mode 100644 packages/atoms/index.ts create mode 100644 packages/atoms/package.json create mode 100644 packages/atoms/postcss.config.js create mode 100644 packages/atoms/tailwind.config.cjs create mode 100644 packages/atoms/tsconfig.json create mode 100644 packages/atoms/types.ts create mode 100644 packages/atoms/vite.config.ts create mode 100644 packages/features/bookings/Booker/Booker.tsx create mode 100644 packages/features/bookings/Booker/components/AvailableTimeSlots.tsx create mode 100644 packages/features/bookings/Booker/components/Away.tsx create mode 100644 packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx create mode 100644 packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx create mode 100644 packages/features/bookings/Booker/components/BookEventForm/Skeleton.tsx create mode 100644 packages/features/bookings/Booker/components/BookEventForm/index.ts create mode 100644 packages/features/bookings/Booker/components/DatePicker.tsx create mode 100644 packages/features/bookings/Booker/components/EventMeta.tsx create mode 100644 packages/features/bookings/Booker/components/LargeCalendar.tsx create mode 100644 packages/features/bookings/Booker/components/Section.tsx create mode 100644 packages/features/bookings/Booker/config.ts create mode 100644 packages/features/bookings/Booker/index.ts create mode 100644 packages/features/bookings/Booker/store.ts create mode 100644 packages/features/bookings/Booker/types.ts create mode 100644 packages/features/bookings/Booker/utils/dates.ts create mode 100644 packages/features/bookings/Booker/utils/event.ts create mode 100644 packages/features/bookings/Booker/utils/query-param.ts create mode 100644 packages/features/bookings/components/AvailableTimes.tsx create mode 100644 packages/features/bookings/components/TimeFormatToggle.tsx create mode 100644 packages/features/bookings/components/event-meta/Details.tsx create mode 100644 packages/features/bookings/components/event-meta/Duration.tsx create mode 100644 packages/features/bookings/components/event-meta/EventMeta.stories.mdx create mode 100644 packages/features/bookings/components/event-meta/Locations.tsx create mode 100644 packages/features/bookings/components/event-meta/Members.tsx create mode 100644 packages/features/bookings/components/event-meta/Occurences.tsx create mode 100644 packages/features/bookings/components/event-meta/Price.tsx create mode 100644 packages/features/bookings/components/event-meta/Skeleton.tsx create mode 100644 packages/features/bookings/components/event-meta/Title.tsx create mode 100644 packages/features/bookings/components/event-meta/event.mock.ts create mode 100644 packages/features/bookings/components/event-meta/index.ts create mode 100644 packages/features/bookings/index.ts create mode 100644 packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx create mode 100644 packages/features/bookings/lib/create-booking.ts rename {apps/web/lib/mutations/bookings => packages/features/bookings/lib}/create-recurring-booking.ts (67%) create mode 100644 packages/features/bookings/lib/get-booking.ts create mode 100644 packages/features/bookings/lib/index.ts create mode 100644 packages/features/bookings/lib/timePreferences.ts create mode 100644 packages/features/bookings/types.ts create mode 100644 packages/features/eventtypes/lib/getPublicEvent.ts create mode 100644 packages/features/schedules/lib/use-schedule/index.ts create mode 100644 packages/features/schedules/lib/use-schedule/types.ts create mode 100644 packages/features/schedules/lib/use-schedule/useNonEmptyScheduleDays.ts create mode 100644 packages/features/schedules/lib/use-schedule/useSchedule.ts create mode 100644 packages/features/schedules/lib/use-schedule/useSlotsForDate.ts create mode 100644 packages/lib/array.ts rename {apps/web/lib/core/http => packages/lib}/fetch-wrapper.ts (97%) rename apps/web/lib/parseDate.ts => packages/lib/parse-dates.ts (62%) rename apps/web/lib/parseZone.ts => packages/lib/parse-zone.ts (100%) diff --git a/.env.example b/.env.example index 4ddb81f5eb..0a75c7390a 100644 --- a/.env.example +++ b/.env.example @@ -178,4 +178,4 @@ CSP_POLICY= # Vercel Edge Config EDGE_CONFIG= -NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes \ No newline at end of file +NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes diff --git a/apps/storybook/.storybook/main.js b/apps/storybook/.storybook/main.js index 3b7382f1ed..9b0d21eada 100644 --- a/apps/storybook/.storybook/main.js +++ b/apps/storybook/.storybook/main.js @@ -4,6 +4,7 @@ module.exports = { stories: [ "../intro.stories.mdx", "../../../packages/ui/components/**/*.stories.mdx", + "../../../packages/atoms/**/*.stories.mdx", "../../../packages/features/**/*.stories.mdx", "../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)", ], @@ -70,4 +71,5 @@ module.exports = { return config; }, + typescript: { reactDocgen: 'react-docgen' } }; diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index ff2dd7ca07..42e9bb454d 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -21,6 +21,7 @@ import { useIsBackgroundTransparent, useIsEmbed, } from "@calcom/embed-core/embed-iframe"; +import { createBooking, createRecurringBooking } from "@calcom/features/bookings/lib"; import { getBookingFieldsWithSystemFields, SystemField, @@ -38,6 +39,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; +import { parseDate, parseRecurringDates } from "@calcom/lib/parse-dates"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { TimeFormat } from "@calcom/lib/timeFormat"; @@ -47,9 +49,6 @@ import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/ import { timeZone } from "@lib/clock"; import useRouterQuery from "@lib/hooks/useRouterQuery"; -import createBooking from "@lib/mutations/bookings/create-booking"; -import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking"; -import { parseRecurringDates, parseDate } from "@lib/parseDate"; import type { Gate, GateState } from "@components/Gates"; import Gates from "@components/Gates"; @@ -433,7 +432,6 @@ const BookingPage = ({ // Calculate the booking date(s) let recurringStrings: string[] = [], recurringDates: Date[] = []; - if (eventType.recurringEvent?.freq && recurringEventCount !== null) { [recurringStrings, recurringDates] = parseRecurringDates( { @@ -443,7 +441,7 @@ const BookingPage = ({ recurringCount: parseInt(recurringEventCount.toString()), selectedTimeFormat: timeFormat, }, - i18n + i18n.language ); } @@ -572,7 +570,7 @@ const BookingPage = ({
{isClientTimezoneAvailable && (rescheduleUid || !eventType.recurringEvent?.freq) && - `${parseDate(date, i18n, timeFormat)}`} + `${parseDate(date, i18n.language, { selectedTimeFormat: timeFormat })}`} {isClientTimezoneAvailable && !rescheduleUid && eventType.recurringEvent?.freq && @@ -602,7 +600,9 @@ const BookingPage = ({ {isClientTimezoneAvailable && typeof booking.startTime === "string" && - parseDate(dayjs(booking.startTime), i18n, timeFormat)} + parseDate(dayjs(booking.startTime), i18n.language, { + selectedTimeFormat: timeFormat, + })}

)} diff --git a/apps/web/components/error/error-page.tsx b/apps/web/components/error/error-page.tsx index b394191129..e3102f230b 100644 --- a/apps/web/components/error/error-page.tsx +++ b/apps/web/components/error/error-page.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { HttpError } from "@lib/core/http/error"; +import { HttpError } from "@calcom/lib/http-error"; type Props = { statusCode?: number | null; diff --git a/apps/web/lib/mutations/bookings/create-booking.ts b/apps/web/lib/mutations/bookings/create-booking.ts deleted file mode 100644 index d0b677adab..0000000000 --- a/apps/web/lib/mutations/bookings/create-booking.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; - -import * as fetch from "@lib/core/http/fetch-wrapper"; -import type { BookingResponse } from "@lib/types/booking"; - -type BookingCreateBodyForMutation = Omit; -const createBooking = async (data: BookingCreateBodyForMutation) => { - const response = await fetch.post("/api/book/event", data); - - return response; -}; - -export default createBooking; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index e3108db4c3..f859499763 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -1,6 +1,7 @@ require("dotenv").config({ path: "../../.env" }); const CopyWebpackPlugin = require("copy-webpack-plugin"); const os = require("os"); +const glob = require("glob"); const { withAxiom } = require("next-axiom"); const { i18n } = require("./next-i18next.config"); @@ -66,6 +67,18 @@ if (process.env.ANALYZE === "true") { } plugins.push(withAxiom); + +/** Needed to rewrite public booking page, gets all static pages but [user] */ +const pages = glob + .sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname }) + .map((filename) => + filename + .substr(6) + .replace(/(\.tsx|\.js|\.ts)/, "") + .replace(/\/.*/, "") + ) + .filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]")); + /** @type {import("next").NextConfig} */ const nextConfig = { i18n, @@ -198,6 +211,16 @@ const nextConfig = { source: "/embed/embed.js", destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?, }, */ + { + source: `/:user((?!${pages.join("|")}).*)/:type`, + destination: "/new-booker/:user/:type", + has: [{ type: "cookie", key: "new-booker-enabled" }], + }, + { + source: "/team/:slug/:type", + destination: "/new-booker/team/:slug/:type", + has: [{ type: "cookie", key: "new-booker-enabled" }], + }, ]; }, async headers() { diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index 1a914ef05d..741f35882d 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -5,6 +5,8 @@ import type { LocationObject } from "@calcom/app-store/locations"; import { privacyFilteredLocations } from "@calcom/app-store/locations"; import { getAppFromSlug } from "@calcom/app-store/utils"; import dayjs from "@calcom/dayjs"; +import getBooking from "@calcom/features/bookings/lib/get-booking"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { parseRecurringEvent } from "@calcom/lib"; import { @@ -13,8 +15,6 @@ import { getGroupName, getUsernameList, } from "@calcom/lib/defaultEvents"; -import getBooking from "@calcom/lib/getBooking"; -import type { GetBookingType } from "@calcom/lib/getBooking"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import prisma, { bookEventTypeSelect } from "@calcom/prisma"; diff --git a/apps/web/pages/_error.tsx b/apps/web/pages/_error.tsx index 59869c4697..fb0ce29505 100644 --- a/apps/web/pages/_error.tsx +++ b/apps/web/pages/_error.tsx @@ -8,10 +8,9 @@ import NextError from "next/error"; import React from "react"; import { getErrorFromUnknown } from "@calcom/lib/errors"; +import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; -import { HttpError } from "@lib/core/http/error"; - import { ErrorPage } from "@components/error/error-page"; // Adds HttpException to the list of possible error types. diff --git a/apps/web/pages/api/auth/oidc.ts b/apps/web/pages/api/auth/oidc.ts index f8cd8799eb..0e65b2b551 100644 --- a/apps/web/pages/api/auth/oidc.ts +++ b/apps/web/pages/api/auth/oidc.ts @@ -1,8 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import jackson from "@calcom/features/ee/sso/lib/jackson"; - -import { HttpError } from "@lib/core/http/error"; +import { HttpError } from "@calcom/lib/http-error"; // This is the callback endpoint for the OIDC provider // A team must set this endpoint in the OIDC provider's configuration diff --git a/apps/web/pages/api/auth/saml/authorize.ts b/apps/web/pages/api/auth/saml/authorize.ts index 8d42bec42e..337406cb4f 100644 --- a/apps/web/pages/api/auth/saml/authorize.ts +++ b/apps/web/pages/api/auth/saml/authorize.ts @@ -2,8 +2,7 @@ import type { OAuthReq } from "@boxyhq/saml-jackson"; import type { NextApiRequest, NextApiResponse } from "next"; import jackson from "@calcom/features/ee/sso/lib/jackson"; - -import type { HttpError } from "@lib/core/http/error"; +import type { HttpError } from "@calcom/lib/http-error"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { oauthController } = await jackson(); diff --git a/apps/web/pages/api/integrations/[...args].ts b/apps/web/pages/api/integrations/[...args].ts index 228b896125..5b789e8ffd 100644 --- a/apps/web/pages/api/integrations/[...args].ts +++ b/apps/web/pages/api/integrations/[...args].ts @@ -4,12 +4,11 @@ import type { Session } from "next-auth"; import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; +import { HttpError } from "@calcom/lib/http-error"; import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache"; import prisma from "@calcom/prisma"; import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler"; -import { HttpError } from "@lib/core/http/error"; - const defaultIntegrationAddHandler = async ({ slug, supportsMultipleInstalls, diff --git a/apps/web/pages/api/newbooker/[status].tsx b/apps/web/pages/api/newbooker/[status].tsx new file mode 100644 index 0000000000..13bc1db346 --- /dev/null +++ b/apps/web/pages/api/newbooker/[status].tsx @@ -0,0 +1,26 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { defaultResponder } from "@calcom/lib/server"; + +const newBookerSchema = z.object({ + status: z.enum(["enable", "disable"]), +}); + +/** + * Very basic temporary api route to enable/disable new booker access. + */ +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { status } = newBookerSchema.parse(req.query); + + if (status === "enable") { + const expires = new Date(); + expires.setFullYear(expires.getFullYear() + 1); + res.setHeader("Set-Cookie", `new-booker-enabled=true; path=/; expires=${expires.toUTCString()}`); + } else { + res.setHeader("Set-Cookie", "new-booker-enabled=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"); + } + res.send({ status: 200, body: `Done – ${status}` }); +} + +export default defaultResponder(handler); diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index b019cb9594..aa25e7e84b 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -10,6 +10,7 @@ import { availabilityAsString } from "@calcom/lib/availability"; import { yyyymmdd } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; +import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc/react"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule"; @@ -35,8 +36,6 @@ import { } from "@calcom/ui"; import { Info, Plus, Trash, MoreHorizontal } from "@calcom/ui/components/icon"; -import { HttpError } from "@lib/core/http/error"; - import PageWrapper from "@components/PageWrapper"; import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader"; import EditableHeading from "@components/ui/EditableHeading"; diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 093992eaf7..8c0cb069d7 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -3,13 +3,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import Shell from "@calcom/features/shell/Shell"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HttpError } from "@calcom/lib/http-error"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { EmptyScreen, showToast } from "@calcom/ui"; import { Clock } from "@calcom/ui/components/icon"; import { withQuery } from "@lib/QueryCell"; -import { HttpError } from "@lib/core/http/error"; import PageWrapper from "@components/PageWrapper"; import SkeletonLoader from "@components/availability/SkeletonLoader"; diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index ca02ce8376..20a5bd6141 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -24,6 +24,7 @@ import { useIsEmbed, } from "@calcom/embed-core/embed-iframe"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking"; import { SystemField, getBookingFieldsWithSystemFields, @@ -36,7 +37,6 @@ import { formatToLocalizedTimezone, } from "@calcom/lib/date-fns"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; -import { getBookingWithResponses } from "@calcom/lib/getBooking"; import useGetBrandingColours from "@calcom/lib/getBrandColours"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; diff --git a/apps/web/pages/d/[link]/[slug].tsx b/apps/web/pages/d/[link]/[slug].tsx index 4ab511ec1b..3017918cdb 100644 --- a/apps/web/pages/d/[link]/[slug].tsx +++ b/apps/web/pages/d/[link]/[slug].tsx @@ -3,9 +3,9 @@ import { z } from "zod"; import type { LocationObject } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { parseRecurringEvent } from "@calcom/lib"; import { getWorkingHours } from "@calcom/lib/availability"; -import type { GetBookingType } from "@calcom/lib/getBooking"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { availiblityPageEventTypeSelect } from "@calcom/prisma"; import prisma from "@calcom/prisma"; diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index e2ec5c7043..0f679ea447 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -17,6 +17,7 @@ import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; +import { HttpError } from "@calcom/lib/http-error"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc, TRPCClientError } from "@calcom/trpc/react"; import { @@ -59,7 +60,6 @@ import { } from "@calcom/ui/components/icon"; import { withQuery } from "@lib/QueryCell"; -import { HttpError } from "@lib/core/http/error"; import { EmbedButton, EmbedDialog } from "@components/Embed"; import PageWrapper from "@components/PageWrapper"; diff --git a/apps/web/pages/new-booker/[user]/[type].tsx b/apps/web/pages/new-booker/[user]/[type].tsx new file mode 100644 index 0000000000..6a6db3e0e8 --- /dev/null +++ b/apps/web/pages/new-booker/[user]/[type].tsx @@ -0,0 +1,112 @@ +import type { GetServerSidePropsContext } from "next"; +import { z } from "zod"; + +import { Booker } from "@calcom/atoms"; +import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; +import { getUsernameList } from "@calcom/lib/defaultEvents"; +import prisma from "@calcom/prisma"; + +import type { inferSSRProps } from "@lib/types/inferSSRProps"; + +import PageWrapper from "@components/PageWrapper"; + +type PageProps = inferSSRProps; + +export default function Type({ slug, user, booking, away }: PageProps) { + return ( +
+ +
+ ); +} + +Type.PageWrapper = PageWrapper; + +async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { + const { user, type: slug } = paramsSchema.parse(context.params); + const { rescheduleUid } = context.query; + + const { ssgInit } = await import("@server/lib/ssg"); + const ssg = await ssgInit(context); + const usernameList = getUsernameList(user); + + const users = await prisma.user.findMany({ + where: { + username: { + in: usernameList, + }, + }, + select: { + allowDynamicBooking: true, + }, + }); + + if (!users.length) { + return { + notFound: true, + }; + } + + let booking: GetBookingType | null = null; + if (rescheduleUid) { + booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`); + } + + return { + props: { + booking, + user, + slug, + away: false, + trpcState: ssg.dehydrate(), + }, + }; +} + +async function getUserPageProps(context: GetServerSidePropsContext) { + const { user: username, type: slug } = paramsSchema.parse(context.params); + const { rescheduleUid } = context.query; + const { ssgInit } = await import("@server/lib/ssg"); + const ssg = await ssgInit(context); + const user = await prisma.user.findUnique({ + where: { + username, + }, + select: { + away: true, + }, + }); + + if (!user) { + return { + notFound: true, + }; + } + + let booking: GetBookingType | null = null; + if (rescheduleUid) { + booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`); + } + + return { + props: { + booking, + away: user?.away, + user: username, + slug, + trpcState: ssg.dehydrate(), + }, + }; +} + +const paramsSchema = z.object({ type: z.string(), user: z.string() }); + +// Booker page fetches a tiny bit of data server side, to determine early +// whether the page should show an away state or dynamic booking not allowed. +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const { user } = paramsSchema.parse(context.params); + const isDynamicGroup = user.includes("+"); + + return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context); +}; diff --git a/apps/web/pages/new-booker/team/[slug]/[type].tsx b/apps/web/pages/new-booker/team/[slug]/[type].tsx new file mode 100644 index 0000000000..46a91e6890 --- /dev/null +++ b/apps/web/pages/new-booker/team/[slug]/[type].tsx @@ -0,0 +1,65 @@ +import type { GetServerSidePropsContext } from "next"; +import { z } from "zod"; + +import { Booker } from "@calcom/atoms"; +import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; +import prisma from "@calcom/prisma"; + +import type { inferSSRProps } from "@lib/types/inferSSRProps"; + +import PageWrapper from "@components/PageWrapper"; + +type PageProps = inferSSRProps; + +export default function Type({ slug, user, booking, away }: PageProps) { + return ( +
+ +
+ ); +} + +Type.PageWrapper = PageWrapper; + +const paramsSchema = z.object({ type: z.string(), slug: z.string() }); + +// Booker page fetches a tiny bit of data server side: +// 1. Check if team exists, to show 404 +// 2. If rescheduling, get the booking details +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params); + const { rescheduleUid } = context.query; + const { ssgInit } = await import("@server/lib/ssg"); + const ssg = await ssgInit(context); + + const team = await prisma.team.findFirst({ + where: { + slug: teamSlug, + }, + select: { + id: true, + }, + }); + + if (!team) { + return { + notFound: true, + }; + } + + let booking: GetBookingType | null = null; + if (rescheduleUid) { + booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`); + } + + return { + props: { + booking, + away: false, + user: teamSlug, + slug: meetingSlug, + trpcState: ssg.dehydrate(), + }, + }; +}; diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 59da20570f..1eeb235b24 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -2,10 +2,10 @@ import type { GetServerSidePropsContext } from "next"; import type { LocationObject } from "@calcom/core/location"; import { privacyFilteredLocations } from "@calcom/core/location"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; +import getBooking from "@calcom/features/bookings/lib/get-booking"; import { parseRecurringEvent } from "@calcom/lib"; import { getWorkingHours } from "@calcom/lib/availability"; -import getBooking from "@calcom/lib/getBooking"; -import type { GetBookingType } from "@calcom/lib/getBooking"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import prisma from "@calcom/prisma"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx index 35d73d6553..6f85ead949 100644 --- a/apps/web/pages/team/[slug]/book.tsx +++ b/apps/web/pages/team/[slug]/book.tsx @@ -3,10 +3,10 @@ import { z } from "zod"; import type { LocationObject } from "@calcom/app-store/locations"; import { privacyFilteredLocations } from "@calcom/app-store/locations"; +import getBooking from "@calcom/features/bookings/lib/get-booking"; +import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { parseRecurringEvent } from "@calcom/lib"; -import type { GetBookingType } from "@calcom/lib/getBooking"; -import getBooking from "@calcom/lib/getBooking"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import prisma from "@calcom/prisma"; import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 7a956a3871..985125be5a 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -1,6 +1,7 @@ import { expect } from "@playwright/test"; import { test } from "./lib/fixtures"; +import { testBothBookers } from "./lib/new-booker"; import { bookFirstEvent, bookOptinEvent, @@ -12,7 +13,7 @@ import { test.describe.configure({ mode: "parallel" }); test.afterEach(async ({ users }) => users.deleteAll()); -test.describe("free user", () => { +testBothBookers.describe("free user", (bookerVariant) => { test.beforeEach(async ({ page, users }) => { const free = await users.create(); await page.goto(`/${free.username}`); @@ -24,12 +25,18 @@ test.describe("free user", () => { await selectFirstAvailableTimeSlotNextMonth(page); - // Navigate to book page - await page.waitForNavigation({ - url(url) { - return url.pathname.endsWith("/book"); - }, - }); + // Kept in if statement here, since it's only temporary + // until the old booker isn't used anymore, and I wanted + // to change the test as little as possible. + // eslint-disable-next-line playwright/no-conditional-in-test + if (bookerVariant !== "new-booker") { + // Navigate to book page + await page.waitForNavigation({ + url(url) { + return url.pathname.endsWith("/book"); + }, + }); + } // save booking url const bookingUrl: string = page.url(); @@ -51,7 +58,7 @@ test.describe("free user", () => { }); }); -test.describe("pro user", () => { +testBothBookers.describe("pro user", () => { test.beforeEach(async ({ page, users }) => { const pro = await users.create(); await page.goto(`/${pro.username}`); diff --git a/apps/web/playwright/booking-seats.e2e.ts b/apps/web/playwright/booking-seats.e2e.ts index 35619f9409..2a872151d6 100644 --- a/apps/web/playwright/booking-seats.e2e.ts +++ b/apps/web/playwright/booking-seats.e2e.ts @@ -7,6 +7,7 @@ import prisma from "@calcom/prisma"; import type { Fixtures } from "./lib/fixtures"; import { test } from "./lib/fixtures"; +import { testBothBookers } from "./lib/new-booker"; import { bookTimeSlot, createNewSeatedEventType, @@ -46,7 +47,7 @@ async function createUserWithSeatedEventAndAttendees( return { user, eventType, booking }; } -test.describe("Booking with Seats", () => { +testBothBookers.describe("Booking with Seats", (bookerVariant) => { test("User can create a seated event (2 seats as example)", async ({ users, page }) => { const user = await users.create({ name: "Seated event" }); await user.login(); @@ -64,11 +65,19 @@ test.describe("Booking with Seats", () => { }); await page.goto(`/${user.username}/${slug}`); await selectFirstAvailableTimeSlotNextMonth(page); - await page.waitForNavigation({ - url(url) { - return url.pathname.endsWith("/book"); - }, - }); + + // Kept in if statement here, since it's only temporary + // until the old booker isn't used anymore, and I wanted + // to change the test as little as possible. + // eslint-disable-next-line playwright/no-conditional-in-test + if (bookerVariant === "old-booker") { + await page.waitForNavigation({ + url(url) { + return url.pathname.endsWith("/book"); + }, + }); + } + const bookingUrl = page.url(); await test.step("Attendee #1 can book a seated event time slot", async () => { await page.goto(bookingUrl); @@ -93,7 +102,7 @@ test.describe("Booking with Seats", () => { // TODO: Make E2E test: All attendees canceling should delete the booking for the User // todo("All attendees canceling should delete the booking for the User"); - test.describe("Reschedule for booking with seats", () => { + testBothBookers.describe("Reschedule for booking with seats", () => { test("Should reschedule booking with seats", async ({ page, users, bookings }) => { const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [ { name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" }, diff --git a/apps/web/playwright/event-types.e2e.ts b/apps/web/playwright/event-types.e2e.ts index bdfe43ae69..d3a12f1622 100644 --- a/apps/web/playwright/event-types.e2e.ts +++ b/apps/web/playwright/event-types.e2e.ts @@ -4,12 +4,13 @@ import { WEBAPP_URL } from "@calcom/lib/constants"; import { randomString } from "@calcom/lib/random"; import { test } from "./lib/fixtures"; +import { testBothBookers } from "./lib/new-booker"; import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; test.describe.configure({ mode: "parallel" }); test.describe("Event Types tests", () => { - test.describe("user", () => { + testBothBookers.describe("user", (bookerVariant) => { test.beforeEach(async ({ page, users }) => { const user = await users.create(); await user.login(); @@ -147,11 +148,17 @@ test.describe("Event Types tests", () => { await selectFirstAvailableTimeSlotNextMonth(page); // Navigate to book page - await page.waitForNavigation({ - url(url) { - return url.pathname.endsWith("/book"); - }, - }); + // Kept in if statement here, since it's only temporary + // until the old booker isn't used anymore, and I wanted + // to change the test as little as possible. + // eslint-disable-next-line playwright/no-conditional-in-test + if (bookerVariant === "old-booker") { + await page.waitForNavigation({ + url(url) { + return url.pathname.endsWith("/book"); + }, + }); + } for (const location of locationData) { await page.locator(`span:has-text("${location}")`).click(); diff --git a/apps/web/playwright/lib/fixtures.ts b/apps/web/playwright/lib/fixtures.ts index 2868cc1c0d..7b86f34f75 100644 --- a/apps/web/playwright/lib/fixtures.ts +++ b/apps/web/playwright/lib/fixtures.ts @@ -1,3 +1,4 @@ +import type { Page } from "@playwright/test"; import { test as base } from "@playwright/test"; import prisma from "@calcom/prisma"; @@ -10,6 +11,7 @@ import { createServersFixture } from "../fixtures/servers"; import { createUsersFixture } from "../fixtures/users"; export interface Fixtures { + page: Page; users: ReturnType; bookings: ReturnType; payments: ReturnType; diff --git a/apps/web/playwright/lib/new-booker.ts b/apps/web/playwright/lib/new-booker.ts new file mode 100644 index 0000000000..7d9fea6057 --- /dev/null +++ b/apps/web/playwright/lib/new-booker.ts @@ -0,0 +1,31 @@ +import { test } from "./fixtures"; + +export type BookerVariants = "new-booker" | "old-booker"; + +const bookerVariants = ["new-booker", "old-booker"]; + +/** + * Small wrapper around test.describe(). + * When using testbothBookers.describe() instead of test.describe(), this will run the specified + * tests twice. One with the old booker, and one with the new booker. It will also add the booker variant + * name to the test name for easier debugging. + * Finally it also adds a parameter bookerVariant to your testBothBooker.describe() callback, which + * can be used to do any conditional rendering in the test for a specific booker variant (should be as little + * as possible). + * + * See apps/web/playwright/booking-pages.e2e.ts for an example. + */ +export const testBothBookers = { + describe: (testName: string, testFn: (bookerVariant: BookerVariants) => void) => { + bookerVariants.forEach((bookerVariant) => { + test.describe(`${testName} -- ${bookerVariant}`, () => { + if (bookerVariant === "new-booker") { + test.beforeEach(({ context }) => { + context.addCookies([{ name: "new-booker-enabled", value: "true", url: "http://localhost:3000" }]); + }); + } + testFn(bookerVariant as BookerVariants); + }); + }); + }, +}; diff --git a/apps/web/playwright/manage-booking-questions.e2e.ts b/apps/web/playwright/manage-booking-questions.e2e.ts index cb0f30d772..7aa5706999 100644 --- a/apps/web/playwright/manage-booking-questions.e2e.ts +++ b/apps/web/playwright/manage-booking-questions.e2e.ts @@ -7,6 +7,8 @@ import { uuid } from "short-uuid"; import prisma from "@calcom/prisma"; import { test } from "./lib/fixtures"; +import { testBothBookers } from "./lib/new-booker"; +import type { BookerVariants } from "./lib/new-booker"; import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; async function getLabelText(field: Locator) { @@ -18,7 +20,7 @@ test.describe("Manage Booking Questions", () => { await users.deleteAll(); }); - test.describe("For User EventType", () => { + testBothBookers.describe("For User EventType", (bookerVariant) => { test("Do a booking with a user added question and verify a few thing in b/w", async ({ page, users, @@ -37,11 +39,11 @@ test.describe("Manage Booking Questions", () => { await firstEventTypeElement.click(); }); - await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver); + await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver, bookerVariant); }); }); - test.describe("For Team EventType", () => { + testBothBookers.describe("For Team EventType", (bookerVariant) => { test("Do a booking with a user added question and verify a few thing in b/w", async ({ page, users, @@ -60,7 +62,7 @@ test.describe("Manage Booking Questions", () => { await firstEventTypeElement.click(); }); - await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver); + await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver, bookerVariant); }); }); }); @@ -73,7 +75,8 @@ async function runTestStepsCommonForTeamAndUserEventType( close: () => import("http").Server; requestList: (import("http").IncomingMessage & { body?: unknown })[]; url: string; - } + }, + bookerVariant: BookerVariants ) { await page.click('[href$="tabName=advanced"]'); @@ -89,7 +92,7 @@ async function runTestStepsCommonForTeamAndUserEventType( }, }); - await doOnFreshPreview(page, context, async (page) => { + await doOnFreshPreview(page, context, bookerVariant, async (page) => { const allFieldsLocator = await expectSystemFieldsToBeThere(page); const userFieldLocator = allFieldsLocator.nth(5); @@ -105,7 +108,7 @@ async function runTestStepsCommonForTeamAndUserEventType( name: "how_are_you", page, }); - await doOnFreshPreview(page, context, async (page) => { + await doOnFreshPreview(page, context, bookerVariant, async (page) => { const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]'); await expect(formBuilderFieldLocator).toBeHidden(); }); @@ -119,7 +122,7 @@ async function runTestStepsCommonForTeamAndUserEventType( }); await test.step('Try to book without providing "How are you?" response', async () => { - await doOnFreshPreview(page, context, async (page) => { + await doOnFreshPreview(page, context, bookerVariant, async (page) => { await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" }); await expectErrorToBeThereFor({ page, name: "how_are_you" }); }); @@ -138,6 +141,7 @@ async function runTestStepsCommonForTeamAndUserEventType( return await doOnFreshPreview( page, context, + bookerVariant, async (page) => { const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]'); await expect(formBuilderFieldLocator).toBeVisible(); @@ -196,8 +200,7 @@ async function runTestStepsCommonForTeamAndUserEventType( await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => { const page = previewTabPage; - await rescheduleFromTheLinkOnPage({ page }); - // await page.pause(); + await rescheduleFromTheLinkOnPage({ page, bookerVariant }); await expectErrorToBeThereFor({ page, name: "rescheduleReason" }); }); } @@ -304,10 +307,11 @@ async function expectErrorToBeThereFor({ page, name }: { page: Page; name: strin async function doOnFreshPreview( page: Page, context: PlaywrightTestArgs["context"], + bookerVariant: BookerVariants, callback: (page: Page) => Promise, persistTab = false ) { - const previewTabPage = await openBookingFormInPreviewTab(context, page); + const previewTabPage = await openBookingFormInPreviewTab(context, page, bookerVariant); await callback(previewTabPage); if (!persistTab) { await previewTabPage.close(); @@ -347,25 +351,39 @@ async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType url.pathname.endsWith("/book"), - }); + if (bookerVariant === "old-booker") { + await page.waitForNavigation({ + url: (url) => url.pathname.endsWith("/book"), + }); + } await page.click('[data-testid="confirm-reschedule-button"]'); } -async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) { +async function openBookingFormInPreviewTab( + context: PlaywrightTestArgs["context"], + page: Page, + bookerVariant: BookerVariants +) { const previewTabPromise = context.waitForEvent("page"); await page.locator('[data-testid="preview-button"]').click(); const previewTabPage = await previewTabPromise; await previewTabPage.waitForLoadState(); await selectFirstAvailableTimeSlotNextMonth(previewTabPage); - await previewTabPage.waitForNavigation({ - url: (url) => url.pathname.endsWith("/book"), - }); + if (bookerVariant === "old-booker") { + await previewTabPage.waitForNavigation({ + url: (url) => url.pathname.endsWith("/book"), + }); + } return previewTabPage; } diff --git a/apps/web/playwright/reschedule.e2e.ts b/apps/web/playwright/reschedule.e2e.ts index 70a648345f..6bab7bd4b6 100644 --- a/apps/web/playwright/reschedule.e2e.ts +++ b/apps/web/playwright/reschedule.e2e.ts @@ -4,6 +4,7 @@ import { BookingStatus } from "@prisma/client"; import prisma from "@calcom/prisma"; import { test } from "./lib/fixtures"; +import { testBothBookers } from "./lib/new-booker"; import { selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; const IS_STRIPE_ENABLED = !!( @@ -16,7 +17,7 @@ test.describe.configure({ mode: "parallel" }); test.afterEach(({ users }) => users.deleteAll()); -test.describe("Reschedule Tests", async () => { +testBothBookers.describe("Reschedule Tests", async () => { test("Should do a booking request reschedule from /bookings", async ({ page, users, bookings }) => { const user = await users.create(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 204a9fb4aa..a24a616af8 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1697,6 +1697,15 @@ "spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings", "no_responses_yet": "No responses yet", "this_will_be_the_placeholder": "This will be the placeholder", + "error_booking_event": "An error occured when booking the event, please refresh the page and try again", + "timeslot_missing_title": "No timeslot selected", + "timeslot_missing_description": "Please select a timeslot to book the event.", + "timeslot_missing_cta": "Select timeslot", + "switch_monthly": "Switch to monthly view", + "switch_weekly": "Switch to weekly view", + "switch_multiday": "Switch to day view", + "num_locations": "{{num}} location options", + "select_on_next_step": "Select on the next step", "this_meeting_has_not_started_yet": "This meeting has not started yet", "this_app_requires_connected_account": "{{appName}} requires a connected {{dependencyName}} account", "connect_app": "Connect {{dependencyName}}", diff --git a/apps/web/test/lib/parseZone.test.ts b/apps/web/test/lib/parseZone.test.ts index d6fee7943a..0883283da9 100644 --- a/apps/web/test/lib/parseZone.test.ts +++ b/apps/web/test/lib/parseZone.test.ts @@ -1,4 +1,4 @@ -import { parseZone } from "@lib/parseZone"; +import { parseZone } from "@calcom/lib/parse-zone"; const EXPECTED_DATE_STRING = "2021-06-20T11:59:59+02:00"; diff --git a/jest.config.ts b/jest.config.ts index a7b3448f64..9fe67b3e5a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,5 +1,8 @@ import type { Config } from "jest"; +// Added +2 to ensure we need to do some conversions in our tests +process.env.TZ = "GMT+2"; + const config: Config = { preset: "ts-jest", verbose: true, @@ -66,6 +69,22 @@ const config: Config = { transformIgnorePatterns: ["/node_modules/", "^.+\\.module\\.(css|sass|scss)$"], testEnvironment: "jsdom", }, + { + displayName: "@calcom/features", + roots: ["/packages/features"], + testMatch: ["**/*.(spec|test).(ts|tsx|js)"], + transform: { + "^.+\\.ts?$": "ts-jest", + }, + transformIgnorePatterns: ["/node_modules/", "^.+\\.module\\.(css|sass|scss)$"], + testEnvironment: "jsdom", + moduleDirectories: ["node_modules", ""], + globals: { + "ts-jest": { + tsconfig: "/packages/features/tsconfig.json", + }, + }, + }, // FIXME: Prevent this breaking Jest when API module is missing // { // displayName: "@calcom/api", diff --git a/packages/atoms/booker/Booker.tsx b/packages/atoms/booker/Booker.tsx new file mode 100644 index 0000000000..28e050bace --- /dev/null +++ b/packages/atoms/booker/Booker.tsx @@ -0,0 +1,13 @@ +import type { BookerProps } from "@calcom/features/bookings/Booker"; +import { Booker as BookerComponent } from "@calcom/features/bookings/Booker"; + +import type { AtomsGlobalConfigProps } from "../types"; + +type BookerAtomProps = BookerProps & AtomsGlobalConfigProps; + +/** + * @TODO Before we can turn this into a reusable atom + * * Use the webAppUrl coming from AtomsGlobalConfigProps to make url dynamic + * * Find a solution for translations + */ +export const Booker = (props: BookerAtomProps) => ; diff --git a/packages/atoms/booker/booker.stories.mdx b/packages/atoms/booker/booker.stories.mdx new file mode 100644 index 0000000000..1d89ee90fd --- /dev/null +++ b/packages/atoms/booker/booker.stories.mdx @@ -0,0 +1,14 @@ +import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; +import { Title } from '@calcom/storybook/components' +import { Icon } from "@calcom/ui"; +import { Booker } from './Booker'; + + + + + +<Canvas> + <Story name="Booker"> + <Booker username="pro" /> + </Story> +</Canvas> diff --git a/packages/atoms/booker/export.ts b/packages/atoms/booker/export.ts new file mode 100644 index 0000000000..2bb3a2ac04 --- /dev/null +++ b/packages/atoms/booker/export.ts @@ -0,0 +1,5 @@ +/** Export file is only used for building the dist version of this Atom. */ +// import "../globals.css"; + +export { Booker } from "./Booker"; +export * from "../types"; diff --git a/packages/atoms/booker/index.ts b/packages/atoms/booker/index.ts new file mode 100644 index 0000000000..47fb01d585 --- /dev/null +++ b/packages/atoms/booker/index.ts @@ -0,0 +1 @@ +export { Booker } from "./Booker"; diff --git a/packages/atoms/build.mjs b/packages/atoms/build.mjs new file mode 100644 index 0000000000..bd5c138cf6 --- /dev/null +++ b/packages/atoms/build.mjs @@ -0,0 +1,31 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import { build } from "vite"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// @TODO: Do we want to automate this by checking all dirs for export.ts? +const libraries = [ + { + entry: path.resolve(__dirname, "./booker/export.ts"), + fileName: "booker", + }, +]; + +libraries.forEach(async (lib) => { + await build({ + build: { + outDir: `./dist/${lib.fileName}`, + lib: { + ...lib, + formats: ["es", "cjs"], + }, + emptyOutDir: false, + }, + resolve: { + alias: { + crypto: require.resolve("rollup-plugin-node-builtins"), + }, + }, + }); +}); diff --git a/packages/atoms/globals.css b/packages/atoms/globals.css new file mode 100644 index 0000000000..47e4bc917b --- /dev/null +++ b/packages/atoms/globals.css @@ -0,0 +1,10 @@ +/* + * @NOTE: This file is only imported when building the component's CSS file + * When using this component in any Cal project, the globals are automatically imported + * in that project. + */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import "../ui/styles/shared-globals.css"; \ No newline at end of file diff --git a/packages/atoms/index.ts b/packages/atoms/index.ts new file mode 100644 index 0000000000..647ca57b8d --- /dev/null +++ b/packages/atoms/index.ts @@ -0,0 +1 @@ +export { Booker } from "./booker/Booker"; diff --git a/packages/atoms/package.json b/packages/atoms/package.json new file mode 100644 index 0000000000..52cf9af7f4 --- /dev/null +++ b/packages/atoms/package.json @@ -0,0 +1,22 @@ +{ + "name": "@calcom/atoms", + "private": true, + "sideEffects": false, + "type": "module", + "description": "Cal.com Atoms", + "authors": "Cal.com, Inc.", + "version": "1.0.0", + "scripts": { + "build": "node build.mjs" + }, + "devDependencies": { + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^2.2.0", + "rollup-plugin-node-builtins": "^2.1.2", + "typescript": "^4.9.3", + "vite": "^3.2.4" + }, + "main": "./index" +} diff --git a/packages/atoms/postcss.config.js b/packages/atoms/postcss.config.js new file mode 100644 index 0000000000..a982c6414e --- /dev/null +++ b/packages/atoms/postcss.config.js @@ -0,0 +1,8 @@ +const config = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + +export default config; diff --git a/packages/atoms/tailwind.config.cjs b/packages/atoms/tailwind.config.cjs new file mode 100644 index 0000000000..55b6825ae4 --- /dev/null +++ b/packages/atoms/tailwind.config.cjs @@ -0,0 +1,7 @@ +const base = require("@calcom/config/tailwind-preset"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + ...base, + content: ["./bookings/**/*.tsx"], +}; diff --git a/packages/atoms/tsconfig.json b/packages/atoms/tsconfig.json new file mode 100644 index 0000000000..b8fb2a6430 --- /dev/null +++ b/packages/atoms/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@calcom/tsconfig/react-library.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["/*"] + }, + "resolveJsonModule": true + }, + "include": [".", "../types/next-auth.d.ts"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/atoms/types.ts b/packages/atoms/types.ts new file mode 100644 index 0000000000..8ebbf932ba --- /dev/null +++ b/packages/atoms/types.ts @@ -0,0 +1,7 @@ +export interface AtomsGlobalConfigProps { + /** + * API endpoint for the Booker component to fetch data from, + * defaults to https://cal.com + */ + webAppUrl?: string; +} diff --git a/packages/atoms/vite.config.ts b/packages/atoms/vite.config.ts new file mode 100644 index 0000000000..0950e3ce06 --- /dev/null +++ b/packages/atoms/vite.config.ts @@ -0,0 +1,28 @@ +import { resolve } from "path"; +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + lib: { + entry: [resolve(__dirname, "booker/export.ts")], + name: "CalAtoms", + fileName: "cal-atoms", + }, + rollupOptions: { + external: ["react", "fs", "path", "os", "react-dom"], + output: { + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, + resolve: { + alias: { + fs: resolve("../../node_modules/rollup-plugin-node-builtins"), + path: resolve("../../node_modules/rollup-plugin-node-builtins"), + os: resolve("../../node_modules/rollup-plugin-node-builtins"), + }, + }, +}); diff --git a/packages/config/package.json b/packages/config/package.json index 99d73b4eab..77ceef3747 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -17,6 +17,7 @@ "eslint-plugin-prettier": "^4.2.1" }, "devDependencies": { + "@savvywombat/tailwindcss-grid-areas": "^3.0.0", "@tailwindcss/forms": "^0.5.2", "@tailwindcss/line-clamp": "^0.4.0", "@tailwindcss/typography": "^0.5.4", diff --git a/packages/config/tailwind-preset.js b/packages/config/tailwind-preset.js index c2dabaa8d2..2c05681336 100644 --- a/packages/config/tailwind-preset.js +++ b/packages/config/tailwind-preset.js @@ -10,6 +10,7 @@ module.exports = { "../../packages/app-store/**/*{components,pages}/**/*.{js,ts,jsx,tsx}", "../../packages/features/**/*.{js,ts,jsx,tsx}", "../../packages/ui/**/*.{js,ts,jsx,tsx}", + "../../packages/atoms/**/*.{js,ts,jsx,tsx}", ], darkMode: "class", theme: { @@ -92,18 +93,12 @@ module.exports = { }, keyframes: { "fade-in-up": { - "0%": { - opacity: 0.75, - transform: "translateY(20px)", - }, - "100%": { - opacity: 1, - transform: "translateY(0)", - }, + from: { opacity: 0, transform: "translateY(10px)" }, + to: { opacity: 1, transform: "none" }, }, }, animation: { - "fade-in-up": "fade-in-up 0.35s cubic-bezier(.21,1.02,.73,1)", + "fade-in-up": "fade-in-up 600ms var(--animation-delay, 0ms) cubic-bezier(.21,1.02,.73,1) forwards", }, boxShadow: { dropdown: "0px 2px 6px -1px rgba(0, 0, 0, 0.08)", @@ -152,6 +147,7 @@ module.exports = { require("@tailwindcss/typography"), require("tailwind-scrollbar"), require("tailwindcss-radix")(), + require("@savvywombat/tailwindcss-grid-areas"), plugin(({ addVariant }) => { addVariant("mac", ".mac &"); addVariant("windows", ".windows &"); diff --git a/packages/features/bookings/Booker/Booker.tsx b/packages/features/bookings/Booker/Booker.tsx new file mode 100644 index 0000000000..1815b7ed27 --- /dev/null +++ b/packages/features/bookings/Booker/Booker.tsx @@ -0,0 +1,213 @@ +import type { MotionStyle } from "framer-motion"; +import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion"; +import { Fragment, useEffect, useRef } from "react"; +import StickyBox from "react-sticky-box"; +import { shallow } from "zustand/shallow"; + +import classNames from "@calcom/lib/classNames"; +import useGetBrandingColours from "@calcom/lib/getBrandColours"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; +import { Logo, ToggleGroup, useCalcomTheme } from "@calcom/ui"; +import { Calendar, Columns, Grid } from "@calcom/ui/components/icon"; + +import { AvailableTimeSlots } from "./components/AvailableTimeSlots"; +import { Away } from "./components/Away"; +import { BookEventForm } from "./components/BookEventForm"; +import { DatePicker } from "./components/DatePicker"; +import { EventMeta } from "./components/EventMeta"; +import { LargeCalendar } from "./components/LargeCalendar"; +import { BookerSection } from "./components/Section"; +import { fadeInUp, fadeInLeft, resizeAnimationConfig } from "./config"; +import { useBookerStore, useInitializeBookerStore } from "./store"; +import type { BookerLayout, BookerProps } from "./types"; +import { useEvent } from "./utils/event"; + +const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => { + const brandTheme = useGetBrandingColours({ + lightVal: brandColor, + darkVal: darkBrandColor, + }); + useCalcomTheme(brandTheme); +}; + +const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: BookerProps) => { + const { t } = useLocale(); + const isMobile = useMediaQuery("(max-width: 768px)"); + const isTablet = useMediaQuery("(max-width: 1024px)"); + const timeslotsRef = useRef<HTMLDivElement>(null); + const StickyOnDesktop = isMobile ? "div" : StickyBox; + const rescheduleUid = + typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null; + const event = useEvent(); + const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); + const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow); + const selectedDate = useBookerStore((state) => state.selectedDate); + const [selectedTimeslot, setSelectedTimeslot] = useBookerStore( + (state) => [state.selectedTimeslot, state.setSelectedTimeslot], + shallow + ); + + useBrandColors({ + brandColor: event.data?.profile.brandColor, + darkBrandColor: event.data?.profile.darkBrandColor, + }); + + useInitializeBookerStore({ + username, + eventSlug, + month, + eventId: event?.data?.id, + rescheduleUid, + rescheduleBooking, + }); + + useEffect(() => { + setLayout(isMobile ? "mobile" : "small_calendar"); + }, [isMobile, setLayout]); + + useEffect(() => { + if (event.isLoading) return setBookerState("loading"); + if (!selectedDate) return setBookerState("selecting_date"); + if (!selectedTimeslot) return setBookerState("selecting_time"); + return setBookerState("booking"); + }, [event, selectedDate, selectedTimeslot, setBookerState]); + + useEffect(() => { + if (layout === "mobile") { + timeslotsRef.current?.scrollIntoView({ behavior: "smooth" }); + } + }, [layout, selectedDate]); + + return ( + <> + {/* + If we would render this on mobile, it would unset the mobile variant, + since that's not a valid option, so it would set the layout to null. + */} + {!isMobile && ( + <div className="[&>div]:bg-muted fixed top-2 right-3 z-10"> + <ToggleGroup + onValueChange={(layout) => setLayout(layout as BookerLayout)} + defaultValue="small_calendar" + options={[ + { + value: "small_calendar", + label: <Calendar width="16" height="16" />, + tooltip: t("switch_monthly"), + }, + { + value: "large_calendar", + label: <Grid width="16" height="16" />, + tooltip: t("switch_weekly"), + }, + { + value: "large_timeslots", + label: <Columns width="16" height="16" />, + tooltip: t("switch_multiday"), + }, + ]} + /> + </div> + )} + <div className="flex h-full w-full flex-col items-center"> + <m.div + layout + // Passing the default animation styles here as the styles, makes sure that there's no initial loading state + // where there's no styles applied yet (meaning there wouldn't be a grid + widths), which would cause + // the layout to jump around on load. + style={resizeAnimationConfig.small_calendar.default as MotionStyle} + animate={resizeAnimationConfig[layout]?.[bookerState] || resizeAnimationConfig[layout].default} + transition={{ ease: "easeInOut", duration: 0.4 }} + className={classNames( + "[--booker-meta-width:280px] [--booker-main-width:480px] [--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]", + "bg-muted grid max-w-full items-start overflow-clip dark:[color-scheme:dark] md:flex-row", + layout === "small_calendar" && + "border-subtle mt-20 min-h-[450px] w-[calc(var(--booker-meta-width)+var(--booker-main-width))] rounded-md border", + layout !== "small_calendar" && "h-auto min-h-screen w-screen" + )}> + <AnimatePresence> + <StickyOnDesktop key="meta" className="relative z-10"> + <BookerSection area="meta" className="max-w-screen w-full md:w-[var(--booker-meta-width)]"> + <EventMeta /> + {layout !== "small_calendar" && !(layout === "mobile" && bookerState === "booking") && ( + <div className=" mt-auto p-6"> + <DatePicker /> + </div> + )} + </BookerSection> + </StickyOnDesktop> + + <BookerSection + key="book-event-form" + area="main" + className="border-subtle sticky top-0 ml-[-1px] h-full p-6 md:w-[var(--booker-main-width)] md:border-l" + {...fadeInUp} + visible={bookerState === "booking"}> + <BookEventForm onCancel={() => setSelectedTimeslot(null)} /> + </BookerSection> + + <BookerSection + key="datepicker" + area="main" + visible={bookerState !== "booking" && layout === "small_calendar"} + {...fadeInUp} + initial="visible" + className="md:border-subtle ml-[-1px] h-full flex-shrink p-6 md:border-l lg:w-[var(--booker-main-width)]"> + <DatePicker /> + </BookerSection> + + <BookerSection + key="large-calendar" + area="main" + visible={ + layout === "large_calendar" && + (bookerState === "selecting_date" || bookerState === "selecting_time") + } + className="border-muted sticky top-0 ml-[-1px] h-full md:border-l" + {...fadeInUp}> + <LargeCalendar /> + </BookerSection> + + <BookerSection + key="timeslots" + area={{ default: "main", small_calendar: "timeslots" }} + visible={ + (layout !== "large_calendar" && bookerState === "selecting_time") || + (layout === "large_timeslots" && bookerState !== "booking") + } + className={classNames( + "border-subtle flex h-full w-full flex-row p-6 pb-0 md:border-l", + layout === "small_calendar" && "h-full overflow-auto md:w-[var(--booker-timeslots-width)]", + layout !== "small_calendar" && "sticky top-0" + )} + ref={timeslotsRef} + {...fadeInLeft}> + <AvailableTimeSlots + extraDays={layout === "large_timeslots" ? (isTablet ? 2 : 4) : 0} + limitHeight={layout === "small_calendar"} + seatsPerTimeslot={event.data?.seatsPerTimeSlot} + /> + </BookerSection> + </AnimatePresence> + </m.div> + + <m.span + key="logo" + className={classNames("mt-auto mb-6 pt-6", layout === "small_calendar" ? "block" : "hidden")}> + <Logo small /> + </m.span> + </div> + </> + ); +}; + +export const Booker = (props: BookerProps) => { + if (props.isAway) return <Away />; + + return ( + <LazyMotion features={domAnimation}> + <BookerComponent {...props} /> + </LazyMotion> + ); +}; diff --git a/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx new file mode 100644 index 0000000000..8cb8711a04 --- /dev/null +++ b/packages/features/bookings/Booker/components/AvailableTimeSlots.tsx @@ -0,0 +1,78 @@ +import { useMemo } from "react"; + +import dayjs from "@calcom/dayjs"; +import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings"; +import { useSlotsForMultipleDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate"; +import { classNames } from "@calcom/lib"; + +import { useBookerStore } from "../store"; +import { useScheduleForEvent } from "../utils/event"; + +type AvailableTimeSlotsProps = { + extraDays?: number; + limitHeight?: boolean; + seatsPerTimeslot?: number | null; +}; + +/** + * Renders available time slots for a given date. + * It will extract the date from the booker store. + * Next to that you can also pass in the `extraDays` prop, this + * will also fetch the next `extraDays` days and show multiple days + * in columns next to each other. + */ +export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeslot }: AvailableTimeSlotsProps) => { + const selectedDate = useBookerStore((state) => state.selectedDate); + const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); + const date = selectedDate || dayjs().format("YYYY-MM-DD"); + + const schedule = useScheduleForEvent({ + prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(), + }); + + // Creates an array of dates to fetch slots for. + // If `extraDays` is passed in, we will extend the array with the next `extraDays` days. + const dates = useMemo( + () => + !extraDays + ? [date] + : [ + // If NO date is selected yet, we show by default the upcomming `nextDays` days. + date, + ...Array.from({ length: extraDays }).map((_, index) => + dayjs(date) + .add(index + 1, "day") + .format("YYYY-MM-DD") + ), + ], + [date, extraDays] + ); + + const isMultipleDates = dates.length > 1; + const slotsPerDay = useSlotsForMultipleDates(dates, schedule?.data?.slots); + + return ( + <div + className={classNames( + limitHeight && "flex-grow md:h-[400px]", + !limitHeight && + "flex w-full flex-row gap-4 [&_header]:top-4 md:[&_header]:top-12 [&_header:before]:h-20" + )}> + {schedule.isLoading + ? // Shows exact amount of days as skeleton. + Array.from({ length: 1 + (extraDays ?? 0) }).map((_, i) => <AvailableTimesSkeleton key={i} />) + : slotsPerDay.length > 0 && + slotsPerDay.map((slots) => ( + <AvailableTimes + className="w-full" + key={slots.date} + showTimeformatToggle={!isMultipleDates} + onTimeSelect={setSelectedTimeslot} + date={dayjs(slots.date)} + slots={slots.slots} + seatsPerTimeslot={seatsPerTimeslot} + /> + ))} + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/Away.tsx b/packages/features/bookings/Booker/components/Away.tsx new file mode 100644 index 0000000000..45109cc97c --- /dev/null +++ b/packages/features/bookings/Booker/components/Away.tsx @@ -0,0 +1,20 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +export const Away = () => { + const { t } = useLocale(); + + return ( + <div className="h-screen"> + <main className="mx-auto max-w-3xl px-4 py-24"> + <div className="space-y-6" data-testid="event-types"> + <div className="border-brand overflow-hidden rounded-sm border"> + <div className="text-subtle p-8 text-center"> + <h2 className="font-cal text-subtle mb-2 text-3xl">😴{" " + t("user_away")}</h2> + <p className="mx-auto max-w-md">{t("user_away_description")}</p> + </div> + </div> + </div> + </main> + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx new file mode 100644 index 0000000000..27f07f6d4f --- /dev/null +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -0,0 +1,342 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import type { UseMutationResult } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/router"; +import { useMemo } from "react"; +import type { FieldError } from "react-hook-form"; +import { useForm } from "react-hook-form"; +import type { TFunction } from "react-i18next"; +import { z } from "zod"; + +import type { EventLocationType } from "@calcom/app-store/locations"; +import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client"; +import dayjs from "@calcom/dayjs"; +import { + useTimePreferences, + mapBookingToMutationInput, + createBooking, + createRecurringBooking, + mapRecurringBookingToMutationInput, +} from "@calcom/features/bookings/lib"; +import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; +import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { HttpError } from "@calcom/lib/http-error"; +import { Form, Button, Alert, EmptyScreen } from "@calcom/ui"; +import { Calendar } from "@calcom/ui/components/icon"; + +import { useBookerStore } from "../../store"; +import { useEvent } from "../../utils/event"; +import { getQueryParam } from "../../utils/query-param"; +import { BookingFields } from "./BookingFields"; +import { FormSkeleton } from "./Skeleton"; + +type BookEventFormProps = { + onCancel?: () => void; +}; + +const getSuccessPath = ({ + uid, + email, + slug, + formerTime, + isRecurring, +}: { + uid: string; + email: string; + slug: string; + formerTime?: string; + isRecurring: boolean; +}) => ({ + pathname: `/booking/${uid}`, + query: { + [isRecurring ? "allRemainingBookings" : "isSuccessBookingPage"]: true, + email: email, + eventTypeSlug: slug, + formerTime: formerTime, + }, +}); + +export const BookEventForm = ({ onCancel }: BookEventFormProps) => { + const router = useRouter(); + const { t, i18n } = useLocale(); + const { timezone } = useTimePreferences(); + const rescheduleUid = useBookerStore((state) => state.rescheduleUid); + const rescheduleBooking = useBookerStore((state) => state.rescheduleBooking); + const eventSlug = useBookerStore((state) => state.eventSlug); + const duration = useBookerStore((state) => state.selectedDuration); + const timeslot = useBookerStore((state) => state.selectedTimeslot); + const recurringEventCount = useBookerStore((state) => state.recurringEventCount); + const username = useBookerStore((state) => state.username); + const isRescheduling = !!rescheduleUid && !!rescheduleBooking; + const event = useEvent(); + const eventType = event.data; + + const defaultValues = useMemo(() => { + if (!eventType?.bookingFields) { + return {}; + } + + const defaultUserValues = { + email: rescheduleUid ? rescheduleBooking?.attendees[0].email : getQueryParam("email") || "", + name: rescheduleUid ? rescheduleBooking?.attendees[0].name : getQueryParam("name") || "", + }; + + if (!isRescheduling) { + const defaults = { + responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>, + }; + + const responses = eventType.bookingFields.reduce((responses, field) => { + return { + ...responses, + [field.name]: getQueryParam(field.name) || undefined, + }; + }, {}); + defaults.responses = { + ...responses, + name: defaultUserValues.name, + email: defaultUserValues.email, + }; + + return defaults; + } + + if (!rescheduleBooking || !rescheduleBooking.attendees.length) { + return {}; + } + const primaryAttendee = rescheduleBooking.attendees[0]; + if (!primaryAttendee) { + return {}; + } + + const defaults = { + responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>, + }; + + const responses = eventType.bookingFields.reduce((responses, field) => { + return { + ...responses, + [field.name]: rescheduleBooking.responses[field.name], + }; + }, {}); + defaults.responses = { + ...responses, + name: defaultUserValues.name, + email: defaultUserValues.email, + }; + return defaults; + }, [eventType?.bookingFields, isRescheduling, rescheduleBooking, rescheduleUid]); + + const bookingFormSchema = z + .object({ + responses: event?.data + ? getBookingResponsesSchema({ + eventType: { bookingFields: getBookingFieldsWithSystemFields(event.data) }, + view: rescheduleUid ? "reschedule" : "booking", + }) + : // Fallback until event is loaded. + z.object({}), + }) + .passthrough(); + + type BookingFormValues = { + locationType?: EventLocationType["type"]; + responses: z.infer<typeof bookingFormSchema>["responses"]; + // Key is not really part of form values, but only used to have a key + // to set generic error messages on. Needed until RHF has implemented root error keys. + globalError: undefined; + }; + + const bookingForm = useForm<BookingFormValues>({ + defaultValues, + resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema + }); + + const createBookingMutation = useMutation(createBooking, { + onSuccess: async (responseData) => { + const { uid, paymentUid } = responseData; + if (paymentUid) { + return await router.push( + createPaymentLink({ + paymentUid, + date: timeslot, + name: bookingForm.getValues("responses.name"), + email: bookingForm.getValues("responses.email"), + absolute: false, + }) + ); + } + + if (!uid) { + console.error("No uid returned from createBookingMutation"); + return; + } + + return await router.push( + getSuccessPath({ + uid, + email: bookingForm.getValues("responses.email"), + formerTime: rescheduleBooking?.startTime + ? dayjs(rescheduleBooking?.startTime).toISOString() + : undefined, + slug: `${eventSlug}`, + isRecurring: false, + }) + ); + }, + }); + + const createRecurringBookingMutation = useMutation(createRecurringBooking, { + onSuccess: async (responseData) => { + const { uid } = responseData[0] || {}; + + if (!uid) { + console.error("No uid returned from createRecurringBookingMutation"); + return; + } + + return await router.push( + getSuccessPath({ + uid, + email: bookingForm.getValues("responses.email"), + slug: `${eventSlug}`, + isRecurring: true, + }) + ); + }, + }); + + if (event.isError) return <Alert severity="warning" message={t("error_booking_event")} />; + if (event.isLoading || !event.data) return <FormSkeleton />; + if (!timeslot) + return ( + <EmptyScreen + headline={t("timeslot_missing_title")} + description={t("timeslot_missing_description")} + Icon={Calendar} + buttonText={t("timeslot_missing_cta")} + buttonOnClick={onCancel} + /> + ); + + const bookEvent = (values: BookingFormValues) => { + bookingForm.clearErrors(); + + // It shouldn't be possible that this method is fired without having event data, + // but since in theory (looking at the types) it is possible, we still handle that case. + if (!event?.data) { + bookingForm.setError("globalError", { message: t("error_booking_event") }); + return; + } + + // Ensures that duration is an allowed value, if not it defaults to the + // default event duration. + const validDuration = + duration && + event.data.metadata?.multipleDuration && + event.data.metadata?.multipleDuration.includes(duration) + ? duration + : event.data.length; + + const bookingInput = { + values, + duration: validDuration, + event: event.data, + date: timeslot, + timeZone: timezone, + language: i18n.language, + rescheduleUid: rescheduleUid || undefined, + username: username || "", + metadata: Object.keys(router.query) + .filter((key) => key.startsWith("metadata")) + .reduce( + (metadata, key) => ({ + ...metadata, + [key.substring("metadata[".length, key.length - 1)]: router.query[key], + }), + {} + ), + }; + + if (event.data?.recurringEvent?.freq && recurringEventCount) { + createRecurringBookingMutation.mutate( + mapRecurringBookingToMutationInput(bookingInput, recurringEventCount) + ); + } else { + createBookingMutation.mutate(mapBookingToMutationInput(bookingInput)); + } + }; + + if (!eventType) { + console.warn("No event type found for event", router.query); + return <Alert severity="warning" message={t("error_booking_event")} />; + } + + return ( + <div className="flex h-full flex-col"> + <Form className="flex h-full flex-col" form={bookingForm} handleSubmit={bookEvent} noValidate> + <BookingFields + isDynamicGroupBooking={!!(username && username.indexOf("+") > -1)} + fields={eventType.bookingFields} + locations={eventType.locations} + rescheduleUid={rescheduleUid || undefined} + /> + + <div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse"> + {!!onCancel && ( + <Button color="minimal" type="button" onClick={onCancel}> + {t("back")} + </Button> + )} + <Button + type="submit" + color="primary" + loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading} + data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}> + {rescheduleUid ? t("reschedule") : t("confirm")} + </Button> + </div> + </Form> + {(createBookingMutation.isError || + createRecurringBookingMutation.isError || + bookingForm.formState.errors["globalError"]) && ( + <div data-testid="booking-fail"> + <Alert + className="mt-2" + severity="info" + title={rescheduleUid ? t("reschedule_fail") : t("booking_fail")} + message={getError( + bookingForm.formState.errors["globalError"], + createBookingMutation, + createRecurringBookingMutation, + t + )} + /> + </div> + )} + </div> + ); +}; + +const getError = ( + globalError: FieldError | undefined, + // It feels like an implementation detail to reimplement the types of useMutation here. + // Since they don't matter for this function, I'd rather disable them then giving you + // the cognitive overload of thinking to update them here when anything changes. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bookingMutation: UseMutationResult<any, any, any, any>, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + recurringBookingMutation: UseMutationResult<any, any, any, any>, + t: TFunction +) => { + if (globalError) return globalError.message; + + const error = bookingMutation.error || recurringBookingMutation.error; + + return error instanceof HttpError || error instanceof Error ? ( + <>{t("can_you_try_again")}</> + ) : ( + "Unknown error" + ); +}; diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx new file mode 100644 index 0000000000..6ba16a63e1 --- /dev/null +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -0,0 +1,119 @@ +import { useFormContext } from "react-hook-form"; + +import type { LocationObject } from "@calcom/app-store/locations"; +import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; +import { FormBuilderField } from "@calcom/features/form-builder"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { RouterOutputs } from "@calcom/trpc/react"; + +import { SystemField } from "../../../lib/getBookingFields"; + +export const BookingFields = ({ + fields, + locations, + rescheduleUid, + isDynamicGroupBooking, +}: { + fields: NonNullable<RouterOutputs["viewer"]["public"]["event"]>["bookingFields"]; + locations: LocationObject[]; + rescheduleUid?: string; + isDynamicGroupBooking: boolean; +}) => { + const { t } = useLocale(); + const { watch, setValue } = useFormContext(); + const locationResponse = watch("responses.location"); + const currentView = rescheduleUid ? "reschedule" : ""; + + return ( + // TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view + <div> + {fields.map((field, index) => { + // During reschedule by default all system fields are readOnly. Make them editable on case by case basis. + // Allowing a system field to be edited might require sending emails to attendees, so we need to be careful + let readOnly = + (field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid; + + let noLabel = false; + let hidden = !!field.hidden; + const fieldViews = field.views; + + if (fieldViews && !fieldViews.find((view) => view.id === currentView)) { + return null; + } + + if (field.name === SystemField.Enum.rescheduleReason) { + // rescheduleReason is a reschedule specific field and thus should be editable during reschedule + readOnly = false; + } + + if (field.name === SystemField.Enum.smsReminderNumber) { + // `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself. + // I think we should have a way to connect 2 fields together and have them share the same value in Form Builder + if (locationResponse?.value === "phone") { + setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue); + // Just don't render the field now, as the value is already connected to attendee phone location + return null; + } + // `smsReminderNumber` can be edited during reschedule even though it's a system field + readOnly = false; + } + + if (field.name === SystemField.Enum.guests) { + // No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests + hidden = isDynamicGroupBooking ? true : !!field.hidden; + } + + // We don't show `notes` field during reschedule + if ( + (field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) && + !!rescheduleUid + ) { + return null; + } + + // Dynamically populate location field options + if (field.name === SystemField.Enum.location && field.type === "radioInput") { + if (!field.optionsInputs) { + throw new Error("radioInput must have optionsInputs"); + } + const optionsInputs = field.optionsInputs; + + // TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field. + const options = getLocationOptionsForSelect(locations, t); + options.forEach((option) => { + const optionInput = optionsInputs[option.value as keyof typeof optionsInputs]; + if (optionInput) { + optionInput.placeholder = option.inputPlaceholder; + } + }); + + field.options = options.filter( + (location): location is NonNullable<(typeof options)[number]> => !!location + ); + // If we have only one option and it has an input, we don't show the field label because Option name acts as label. + // e.g. If it's just Attendee Phone Number option then we don't show `Location` label + if (field.options.length === 1) { + if (field.optionsInputs[field.options[0].value]) { + noLabel = true; + } else { + // If there's only one option and it doesn't have an input, we don't show the field at all because it's visible in the left side bar + hidden = true; + } + } + } + + const label = noLabel ? "" : field.label || t(field.defaultLabel || ""); + const placeholder = field.placeholder || t(field.defaultPlaceholder || ""); + + return ( + <FormBuilderField + className="mb-4" + field={{ ...field, label, placeholder, hidden }} + readOnly={readOnly} + key={index} + /> + ); + })} + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/BookEventForm/Skeleton.tsx b/packages/features/bookings/Booker/components/BookEventForm/Skeleton.tsx new file mode 100644 index 0000000000..5da8cbabd5 --- /dev/null +++ b/packages/features/bookings/Booker/components/BookEventForm/Skeleton.tsx @@ -0,0 +1,29 @@ +import { SkeletonText } from "@calcom/ui"; + +export const FormSkeleton = () => ( + <div className="flex flex-col"> + <SkeletonText className="h-7 w-32" /> + <SkeletonText className="mt-2 h-7 w-full" /> + <SkeletonText className="mt-4 h-7 w-28" /> + <SkeletonText className="mt-2 h-7 w-full" /> + + <div className="mt-12 flex h-7 w-full flex-row items-center gap-4"> + <SkeletonText className="inline h-4 w-4 rounded-full" /> + <SkeletonText className="inline h-7 w-32" /> + </div> + <div className="mt-2 flex h-7 w-full flex-row items-center gap-4"> + <SkeletonText className="inline h-4 w-4 rounded-full" /> + <SkeletonText className="inline h-7 w-28" /> + </div> + + <SkeletonText className="mt-8 h-7 w-32" /> + <SkeletonText className="mt-2 h-7 w-full" /> + <SkeletonText className="mt-4 h-7 w-28" /> + <SkeletonText className="mt-2 h-7 w-full" /> + + <div className="mt-6 flex flex-row gap-3"> + <SkeletonText className="ml-auto h-8 w-20" /> + <SkeletonText className="h-8 w-20" /> + </div> + </div> +); diff --git a/packages/features/bookings/Booker/components/BookEventForm/index.ts b/packages/features/bookings/Booker/components/BookEventForm/index.ts new file mode 100644 index 0000000000..323719ddb6 --- /dev/null +++ b/packages/features/bookings/Booker/components/BookEventForm/index.ts @@ -0,0 +1 @@ +export { BookEventForm } from "./BookEventForm"; diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx new file mode 100644 index 0000000000..e0679b73f6 --- /dev/null +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { shallow } from "zustand/shallow"; + +import type { Dayjs } from "@calcom/dayjs"; +import dayjs from "@calcom/dayjs"; +import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; +import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; +import { weekdayToWeekIndex } from "@calcom/lib/date-fns"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + +import { useBookerStore } from "../store"; +import { useEvent, useScheduleForEvent } from "../utils/event"; + +export const DatePicker = () => { + const [isLoadedClientSide, setIsLoadedClientSide] = useState(false); + const { i18n } = useLocale(); + const [month, selectedDate] = useBookerStore((state) => [state.month, state.selectedDate], shallow); + const [setSelectedDate, setMonth] = useBookerStore( + (state) => [state.setSelectedDate, state.setMonth], + shallow + ); + const event = useEvent(); + const schedule = useScheduleForEvent(); + const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots); + + // Not rendering the component on the server side to avoid hydration issues + // @TODO: We should update the datepicker component as soon as the current booker isn't + // used anymore, so we don't need to have this check. + useEffect(() => { + setIsLoadedClientSide(true); + }, []); + + if (!isLoadedClientSide) return null; + + return ( + <div className="mt-1"> + <DatePickerComponent + isLoading={schedule.isLoading} + onChange={(date: Dayjs) => { + setSelectedDate(date.format("YYYY-MM-DD")); + }} + onMonthChange={(date: Dayjs) => { + setMonth(date.format("YYYY-MM")); + setSelectedDate(date.format("YYYY-MM-DD")); + }} + includedDates={nonEmptyScheduleDays} + locale={i18n.language} + browsingDate={month ? dayjs(month) : undefined} + selected={dayjs(selectedDate)} + weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)} + /> + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx new file mode 100644 index 0000000000..b2b05f5856 --- /dev/null +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -0,0 +1,93 @@ +import { m } from "framer-motion"; +import dynamic from "next/dynamic"; + +import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; +import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; +import { useTimePreferences } from "@calcom/features/bookings/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Calendar, Globe } from "@calcom/ui/components/icon"; + +import { fadeInUp } from "../config"; +import { useBookerStore } from "../store"; +import { formatEventFromToTime } from "../utils/dates"; +import { useEvent } from "../utils/event"; + +const TimezoneSelect = dynamic(() => import("@calcom/ui").then((mod) => mod.TimezoneSelect), { + ssr: false, +}); + +export const EventMeta = () => { + const { timezone, setTimezone, timeFormat } = useTimePreferences(); + const selectedDuration = useBookerStore((state) => state.selectedDuration); + const selectedTimeslot = useBookerStore((state) => state.selectedTimeslot); + const bookerState = useBookerStore((state) => state.state); + const rescheduleBooking = useBookerStore((state) => state.rescheduleBooking); + const { i18n, t } = useLocale(); + const { data: event, isLoading } = useEvent(); + + return ( + <div className="relative z-10 p-6"> + {isLoading && ( + <m.div {...fadeInUp} initial="visible" layout> + <EventMetaSkeleton /> + </m.div> + )} + {!isLoading && !!event && ( + <m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}> + <EventMembers schedulingType={event.schedulingType} users={event.users} profile={event.profile} /> + <EventTitle className="mt-2 mb-8">{event?.title}</EventTitle> + <div className="space-y-4"> + {rescheduleBooking && ( + <EventMetaBlock icon={Calendar}> + {t("former_time")} + <br /> + <span className="line-through" data-testid="former_time_p"> + {formatEventFromToTime( + rescheduleBooking.startTime.toString(), + null, + timeFormat, + timezone, + i18n.language + )} + </span> + </EventMetaBlock> + )} + {selectedTimeslot && ( + <EventMetaBlock icon={Calendar}> + {formatEventFromToTime( + selectedTimeslot, + selectedDuration, + timeFormat, + timezone, + i18n.language + )} + </EventMetaBlock> + )} + <EventDetails event={event} /> + <EventMetaBlock + className="cursor-pointer [&_.current-timezone:before]:focus-within:opacity-100 [&_.current-timezone:before]:hover:opacity-100 [&_>svg]:mt-[4px]" + contentClassName="relative" + icon={Globe}> + {bookerState === "booking" ? ( + <>{timezone}</> + ) : ( + <span className="current-timezone before:bg-subtle flex items-center justify-center before:absolute before:inset-0 before:left-[-30px] before:top-[-3px] before:bottom-[-3px] before:w-[calc(100%_+_35px)] before:rounded-md before:py-3 before:opacity-0 before:transition-opacity"> + <TimezoneSelect + menuPosition="fixed" + classNames={{ + control: () => "!min-h-0 p-0 border-0 bg-transparent focus-within:ring-0", + menu: () => "!w-64 max-w-[90vw]", + singleValue: () => "text-text py-1", + }} + value={timezone} + onChange={(tz) => setTimezone(tz.value)} + /> + </span> + )} + </EventMetaBlock> + </div> + </m.div> + )} + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/LargeCalendar.tsx b/packages/features/bookings/Booker/components/LargeCalendar.tsx new file mode 100644 index 0000000000..155e9c6a94 --- /dev/null +++ b/packages/features/bookings/Booker/components/LargeCalendar.tsx @@ -0,0 +1,30 @@ +import { shallow } from "zustand/shallow"; + +import dayjs from "@calcom/dayjs"; + +import { useBookerStore } from "../store"; + +export const LargeCalendar = () => { + const [setSelectedDate, setSelectedTimeslot] = useBookerStore( + (state) => [state.setSelectedDate, state.setSelectedTimeslot], + shallow + ); + + return ( + <div className="bg-muted flex h-full w-full flex-col items-center justify-center"> + Something big is coming... + <br /> + <button + className="max-w-[300px] underline" + type="button" + onClick={(ev) => { + ev.preventDefault(); + setSelectedDate(dayjs().format("YYYY-MM-DD")); + setSelectedTimeslot(dayjs().format()); + }}> + Click this button to set date + time in one go just like the big thing that is coming here would do. + :) + </button> + </div> + ); +}; diff --git a/packages/features/bookings/Booker/components/Section.tsx b/packages/features/bookings/Booker/components/Section.tsx new file mode 100644 index 0000000000..4c1a039d1a --- /dev/null +++ b/packages/features/bookings/Booker/components/Section.tsx @@ -0,0 +1,62 @@ +import type { MotionProps } from "framer-motion"; +import { m } from "framer-motion"; +import { forwardRef } from "react"; + +import { classNames } from "@calcom/lib"; + +import { useBookerStore } from "../store"; +import type { BookerAreas, BookerLayout } from "../types"; + +/** + * Define what grid area a section should be in. + * Value is either a string (in case it's always the same area), or an object + * looking like: + * { + * // Where default is the required default area. + * default: "calendar", + * // Any optional overrides for different layouts by their layout name. + * large_calendar: "main", + * } + */ +type GridArea = BookerAreas | ({ [key in BookerLayout]?: BookerAreas } & { default: BookerAreas }); + +type BookerSectionProps = { + children: React.ReactNode; + area: GridArea; + visible?: boolean; + className?: string; +} & MotionProps; + +// This map with strings is needed so Tailwind generates all classnames, +// If we would concatenate them with JS, Tailwind would not generate them. +const gridAreaClassNameMap: { [key in BookerAreas]: string } = { + calendar: "[grid-area:calendar]", + main: "[grid-area:main]", + meta: "[grid-area:meta]", + timeslots: "[grid-area:timeslots]", +}; + +/** + * Small helper compnent that renders a booker section in a specific grid area. + */ +export const BookerSection = forwardRef<HTMLDivElement, BookerSectionProps>(function BookerSection( + { children, area, visible, className, ...props }, + ref +) { + const layout = useBookerStore((state) => state.layout); + let gridClassName: string; + + if (typeof area === "string") { + gridClassName = gridAreaClassNameMap[area]; + } else { + gridClassName = gridAreaClassNameMap[area[layout] || area.default]; + } + + if (!visible && typeof visible !== "undefined") return null; + + return ( + <m.div ref={ref} className={classNames(gridClassName, className)} layout {...props}> + {children} + </m.div> + ); +}); diff --git a/packages/features/bookings/Booker/config.ts b/packages/features/bookings/Booker/config.ts new file mode 100644 index 0000000000..33a9c9aff7 --- /dev/null +++ b/packages/features/bookings/Booker/config.ts @@ -0,0 +1,82 @@ +import type { TargetAndTransition } from "framer-motion"; + +import type { BookerLayout, BookerState } from "./types"; + +// Framer motion fade in animation configs. +export const fadeInLeft = { + variants: { + visible: { opacity: 1, x: 0 }, + hidden: { opacity: 0, x: 20 }, + }, + initial: "hidden", + exit: "hidden", + animate: "visible", + transition: { ease: "easeInOut", delay: 0.1 }, +}; +export const fadeInUp = { + variants: { + visible: { opacity: 1, y: 0 }, + hidden: { opacity: 0, y: 20 }, + }, + initial: "hidden", + exit: "hidden", + animate: "visible", + transition: { ease: "easeInOut", delay: 0.1 }, +}; + +type ResizeAnimationConfig = { + [key in BookerLayout]: { + [key in BookerState | "default"]?: TargetAndTransition; + }; +}; + +/** + * This configuration is used to animate the grid container for the booker. + * The object is structured as following: + * + * The root property of the object: is the name of the layout + * (mobile, small_calendar, large_calendar, large_timeslots) + * + * The values of these properties are objects that define the animation for each state of the booker. + * The animation have the same properties as you could pass to the animate prop of framer-motion: + * @see: https://www.framer.com/motion/animation/ + */ +export const resizeAnimationConfig: ResizeAnimationConfig = { + mobile: { + default: { + width: "100%", + gridTemplateAreas: ` + "meta" + "main" + "timeslots" + `, + gridTemplateColumns: "100%", + }, + }, + small_calendar: { + default: { + width: "calc(var(--booker-meta-width) + var(--booker-main-width))", + gridTemplateAreas: `"meta main"`, + gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width)", + }, + selecting_time: { + width: "calc(var(--booker-meta-width) + var(--booker-main-width) + var(--booker-timeslots-width))", + gridTemplateAreas: `"meta main timeslots"`, + gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width) var(--booker-timeslots-width)", + }, + }, + large_calendar: { + default: { + width: "100%", + gridTemplateAreas: `"meta main"`, + gridTemplateColumns: "var(--booker-meta-width) 1fr", + }, + }, + large_timeslots: { + default: { + width: "100%", + gridTemplateAreas: `"meta main"`, + gridTemplateColumns: "var(--booker-meta-width) 1fr", + }, + }, +}; diff --git a/packages/features/bookings/Booker/index.ts b/packages/features/bookings/Booker/index.ts new file mode 100644 index 0000000000..8d7ec08eec --- /dev/null +++ b/packages/features/bookings/Booker/index.ts @@ -0,0 +1,2 @@ +export { Booker } from "./Booker"; +export type { BookerProps } from "./types"; diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts new file mode 100644 index 0000000000..7716e575c1 --- /dev/null +++ b/packages/features/bookings/Booker/store.ts @@ -0,0 +1,168 @@ +import { useEffect } from "react"; +import { create } from "zustand"; + +import dayjs from "@calcom/dayjs"; + +import type { GetBookingType } from "../lib/get-booking"; +import type { BookerState, BookerLayout } from "./types"; +import { updateQueryParam, getQueryParam } from "./utils/query-param"; + +/** + * Arguments passed into store initializer, containing + * the event data. + */ +type StoreInitializeType = { + username: string; + eventSlug: string; + // Month can be undefined if it's not passed in as a prop. + month?: string; + eventId: number | undefined; + rescheduleUid: string | null; + rescheduleBooking: GetBookingType | null | undefined; +}; + +type BookerStore = { + /** + * Event details. These are stored in store for easier + * access in child components. + */ + username: string | null; + eventSlug: string | null; + eventId: number | null; + /** + * Current month being viewed. Format is YYYY-MM. + */ + month: string | null; + setMonth: (month: string | null) => void; + /** + * Current state of the booking process + * the user is currently in. See enum for possible values. + */ + state: BookerState; + setState: (state: BookerState) => void; + /** + * The booker component supports different layouts, + * this value tracks the current layout. + */ + layout: BookerLayout; + setLayout: (layout: BookerLayout) => void; + /** + * Date selected by user (exact day). Format is YYYY-MM-DD. + */ + selectedDate: string | null; + setSelectedDate: (date: string | null) => void; + /** + * Selected event duration in minutes. + */ + selectedDuration: number | null; + setSelectedDuration: (duration: number | null) => void; + /** + * Selected timeslot user has chosen. This is a date string + * containing both the date + time. + */ + selectedTimeslot: string | null; + setSelectedTimeslot: (timeslot: string | null) => void; + /** + * Number of recurring events to create. + */ + recurringEventCount: number | null; + setRecurringEventCount(count: number | null): void; + /** + * If booking is being rescheduled, both the ID as well as + * the current booking details are passed in. The `rescheduleBooking` + * object is something that's fetched server side. + */ + rescheduleUid: string | null; + rescheduleBooking: GetBookingType | null; + /** + * Method called by booker component to set initial data. + */ + initialize: (data: StoreInitializeType) => void; +}; + +/** + * The booker store contains the data of the component's + * current state. This data can be reused within child components + * by importing this hook. + * + * See comments in interface above for more information on it's specific values. + */ +export const useBookerStore = create<BookerStore>((set, get) => ({ + state: "loading", + setState: (state: BookerState) => set({ state }), + layout: "small_calendar", + setLayout: (layout: BookerLayout) => set({ layout }), + selectedDate: getQueryParam("date") || null, + setSelectedDate: (selectedDate: string | null) => { + set({ selectedDate }); + updateQueryParam("date", selectedDate ?? ""); + }, + username: null, + eventSlug: null, + eventId: null, + month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"), + setMonth: (month: string | null) => { + set({ month, selectedTimeslot: null }); + updateQueryParam("month", month ?? ""); + get().setSelectedDate(null); + }, + initialize: ({ + username, + eventSlug, + month, + eventId, + rescheduleUid = null, + rescheduleBooking = null, + }: StoreInitializeType) => { + if ( + get().username === username && + get().eventSlug === eventSlug && + get().month === month && + get().eventId === eventId && + get().rescheduleUid === rescheduleUid && + get().rescheduleBooking?.responses.email === rescheduleBooking?.responses.email + ) + return; + set({ + username, + eventSlug, + eventId, + rescheduleUid, + rescheduleBooking, + }); + // Unset selected timeslot if user is rescheduling. This could happen + // if the user reschedules a booking right after the confirmation page. + // In that case the time would still be store in the store, this way we + // force clear this. + if (rescheduleBooking) set({ selectedTimeslot: null }); + if (month) set({ month }); + }, + selectedDuration: Number(getQueryParam("duration")) || null, + setSelectedDuration: (selectedDuration: number | null) => { + set({ selectedDuration }); + updateQueryParam("duration", selectedDuration ?? ""); + }, + recurringEventCount: null, + setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }), + rescheduleBooking: null, + rescheduleUid: null, + selectedTimeslot: getQueryParam("slot") || null, + setSelectedTimeslot: (selectedTimeslot: string | null) => { + set({ selectedTimeslot }); + updateQueryParam("slot", selectedTimeslot ?? ""); + }, +})); + +export const useInitializeBookerStore = ({ + username, + eventSlug, + month, + eventId, + rescheduleUid = null, + rescheduleBooking = null, +}: StoreInitializeType) => { + const initializeStore = useBookerStore((state) => state.initialize); + useEffect(() => { + initializeStore({ username, eventSlug, month, eventId, rescheduleUid, rescheduleBooking }); + }, [initializeStore, username, eventSlug, month, eventId, rescheduleUid, rescheduleBooking]); +}; diff --git a/packages/features/bookings/Booker/types.ts b/packages/features/bookings/Booker/types.ts new file mode 100644 index 0000000000..54687bd12c --- /dev/null +++ b/packages/features/bookings/Booker/types.ts @@ -0,0 +1,46 @@ +import type { GetBookingType } from "../lib/get-booking"; + +export interface BookerProps { + eventSlug: string; + username: string; + + /** + * If month is NOT set as a prop on the component, we expect a query parameter + * called `month` to be present on the url. If that is missing, the component will + * default to the current month. + * @note In case you're using a client side router, please pass the value in as a prop, + * since the component will leverage window.location, which might not have the query param yet. + * @format YYYY-MM. + * @optional + */ + month?: string; + /** + * Default selected date for with the slotpicker will already open. + * @optional + */ + selectedDate?: Date; + + hideBranding?: boolean; + /** + * Sets the Booker component to the away state. + * This is NOT revalidated by calling the API. + */ + isAway?: boolean; + /** + * If false and the current username indicates a dynamic booking, + * the Booker will immediately show an error. + * This is NOT revalidated by calling the API. + */ + allowsDynamicBooking?: boolean; + /** + * When rescheduling a booking, the current' bookings data is passed in via this prop. + * The component itself won't fetch booking data based on the ID, since there is not public + * api to fetch this data. Therefore rescheduling a booking currently is not possible + * within the atom (i.e. without a server side component). + */ + rescheduleBooking?: GetBookingType; +} + +export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking"; +export type BookerLayout = "small_calendar" | "large_timeslots" | "large_calendar" | "mobile"; +export type BookerAreas = "calendar" | "timeslots" | "main" | "meta"; diff --git a/packages/features/bookings/Booker/utils/dates.ts b/packages/features/bookings/Booker/utils/dates.ts new file mode 100644 index 0000000000..7306119b08 --- /dev/null +++ b/packages/features/bookings/Booker/utils/dates.ts @@ -0,0 +1,18 @@ +import dayjs from "@calcom/dayjs"; +import type { TimeFormat } from "@calcom/lib/timeFormat"; + +export const formatEventFromToTime = ( + date: string, + duration: number | null, + timeFormat: TimeFormat, + timeZone: string, + language: string +) => { + const start = dayjs(date).tz(timeZone); + const end = duration ? start.add(duration, "minute") : null; + return `${start.format("dddd")}, ${start + .toDate() + .toLocaleDateString(language, { dateStyle: "long" })} ${start.format(timeFormat)} ${ + end ? `– ${end.format(timeFormat)}` : `` + }`; +}; diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts new file mode 100644 index 0000000000..3bdebef3f4 --- /dev/null +++ b/packages/features/bookings/Booker/utils/event.ts @@ -0,0 +1,53 @@ +import { shallow } from "zustand/shallow"; + +import { useSchedule } from "@calcom/features/schedules"; +import { trpc } from "@calcom/trpc/react"; + +import { useTimePreferences } from "../../lib/timePreferences"; +import { useBookerStore } from "../store"; + +/** + * Wrapper hook around the trpc query that fetches + * the event curently viewed in the booker. It will get + * the current event slug and username from the booker store. + * + * Using this hook means you only need to use one hook, instead + * of combining multiple conditional hooks. + */ +export const useEvent = () => { + const [username, eventSlug] = useBookerStore((state) => [state.username, state.eventSlug], shallow); + + return trpc.viewer.public.event.useQuery( + { username: username ?? "", eventSlug: eventSlug ?? "" }, + { refetchOnWindowFocus: false, enabled: Boolean(username) && Boolean(eventSlug) } + ); +}; + +/** + * Gets schedule for the current event and current month. + * Gets all values from the booker store. + * + * Using this hook means you only need to use one hook, instead + * of combining multiple conditional hooks. + * + * The prefetchNextMonth argument can be used to prefetch two months at once, + * useful when the user is viewing dates near the end of the month, + * this way the multi day view will show data of both months. + */ +export const useScheduleForEvent = ({ prefetchNextMonth }: { prefetchNextMonth?: boolean } = {}) => { + const { timezone } = useTimePreferences(); + const event = useEvent(); + const [username, eventSlug, month] = useBookerStore( + (state) => [state.username, state.eventSlug, state.month], + shallow + ); + + return useSchedule({ + username, + eventSlug, + eventId: event.data?.id, + month, + timezone, + prefetchNextMonth, + }); +}; diff --git a/packages/features/bookings/Booker/utils/query-param.ts b/packages/features/bookings/Booker/utils/query-param.ts new file mode 100644 index 0000000000..efa7617371 --- /dev/null +++ b/packages/features/bookings/Booker/utils/query-param.ts @@ -0,0 +1,13 @@ +export const updateQueryParam = (param: string, value: string | number) => { + if (typeof window === "undefined") return; + + const url = new URL(window.location.href); + url.searchParams.set(param, `${value}`); + window.history.pushState({}, "", url.href); +}; + +export const getQueryParam = (param: string) => { + if (typeof window === "undefined") return; + + return new URLSearchParams(window.location.search).get(param); +}; diff --git a/packages/features/bookings/components/AvailableTimes.tsx b/packages/features/bookings/components/AvailableTimes.tsx new file mode 100644 index 0000000000..bd8ce620d3 --- /dev/null +++ b/packages/features/bookings/components/AvailableTimes.tsx @@ -0,0 +1,104 @@ +import type { Dayjs } from "@calcom/dayjs"; +import dayjs from "@calcom/dayjs"; +import type { Slots } from "@calcom/features/schedules"; +import { classNames } from "@calcom/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { nameOfDay } from "@calcom/lib/weekday"; +import { Button, SkeletonText } from "@calcom/ui"; + +import { useTimePreferences } from "../lib"; +import { TimeFormatToggle } from "./TimeFormatToggle"; + +type AvailableTimesProps = { + date: Dayjs; + slots: Slots[string]; + onTimeSelect: (time: string) => void; + seatsPerTimeslot?: number | null; + showTimeformatToggle?: boolean; + className?: string; +}; + +export const AvailableTimes = ({ + date, + slots, + onTimeSelect, + seatsPerTimeslot, + showTimeformatToggle = true, + className, +}: AvailableTimesProps) => { + const { t, i18n } = useLocale(); + const [timeFormat, timezone] = useTimePreferences((state) => [state.timeFormat, state.timezone]); + const hasTimeSlots = !!seatsPerTimeslot; + + return ( + <div className={classNames("dark:text-white", className)}> + <header className="bg-muted before:bg-muted sticky top-0 left-0 z-10 mb-8 flex w-full flex-row items-center before:absolute before:-top-12 before:h-24 before:w-full md:flex-col md:items-start lg:flex-row lg:items-center"> + <span className="relative z-10"> + <span className="text-text font-semibold"> + {nameOfDay(i18n.language, Number(date.format("d")), "short")}, + </span> + <span className="dark:text-darkgray-500 text-gray-500"> + {" "} + {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")} + </span> + </span> + + {showTimeformatToggle && ( + <div className="ml-auto md:ml-0 lg:ml-auto"> + <TimeFormatToggle /> + </div> + )} + </header> + <div className="pb-4"> + {!slots.length && ( + <p className={classNames("text-emphasis", showTimeformatToggle ? "-mt-1 text-lg" : "text-sm")}> + {t("all_booked_today")} + </p> + )} + + {slots.map((slot) => { + const bookingFull = !!(hasTimeSlots && slot.attendees && slot.attendees >= seatsPerTimeslot); + return ( + <Button + key={slot.time} + disabled={bookingFull} + data-testid="time" + data-time={slot.time} + onClick={() => onTimeSelect(slot.time)} + className="mb-3 flex h-auto min-h-[44px] w-full flex-col items-center justify-center py-2" + color="secondary"> + {dayjs.utc(slot.time).tz(timezone).format(timeFormat)} + {bookingFull && <p className="text-sm">{t("booking_full")}</p>} + {hasTimeSlots && !bookingFull && ( + <p className="flex items-center text-sm lowercase"> + <span + className={classNames( + slot.attendees && slot.attendees / seatsPerTimeslot >= 0.8 + ? "bg-rose-600" + : slot.attendees && slot.attendees / seatsPerTimeslot >= 0.33 + ? "bg-yellow-500" + : "bg-emerald-400", + "mr-1 inline-block h-2 w-2 rounded-full" + )} + aria-hidden + /> + {slot.attendees ? seatsPerTimeslot - slot.attendees : seatsPerTimeslot}{" "} + {t("seats_available")} + </p> + )} + </Button> + ); + })} + </div> + </div> + ); +}; + +export const AvailableTimesSkeleton = () => ( + <div className="mt-8 flex h-full w-[20%] flex-col only:w-full"> + {/* Random number of elements between 1 and 10. */} + {Array.from({ length: Math.floor(Math.random() * 10) + 1 }).map((_, i) => ( + <SkeletonText className="mb-4 h-6 w-full" key={i} /> + ))} + </div> +); diff --git a/packages/features/bookings/components/TimeFormatToggle.tsx b/packages/features/bookings/components/TimeFormatToggle.tsx new file mode 100644 index 0000000000..71add8b05b --- /dev/null +++ b/packages/features/bookings/components/TimeFormatToggle.tsx @@ -0,0 +1,25 @@ +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { TimeFormat } from "@calcom/lib/timeFormat"; +import { ToggleGroup } from "@calcom/ui"; + +import { useTimePreferences } from "../lib"; + +export const TimeFormatToggle = () => { + const timeFormat = useTimePreferences((state) => state.timeFormat); + const setTimeFormat = useTimePreferences((state) => state.setTimeFormat); + const { t } = useLocale(); + + return ( + <ToggleGroup + onValueChange={(newFormat) => { + if (newFormat !== timeFormat) setTimeFormat(newFormat as TimeFormat); + }} + defaultValue={timeFormat} + value={timeFormat} + options={[ + { value: TimeFormat.TWELVE_HOUR, label: t("12_hour_short") }, + { value: TimeFormat.TWENTY_FOUR_HOUR, label: t("24_hour_short") }, + ]} + /> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Details.tsx b/packages/features/bookings/components/event-meta/Details.tsx new file mode 100644 index 0000000000..f1e68b0d1c --- /dev/null +++ b/packages/features/bookings/components/event-meta/Details.tsx @@ -0,0 +1,168 @@ +import { Fragment } from "react"; +import React from "react"; + +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Info, Clock, CheckSquare, RefreshCcw, CreditCard } from "@calcom/ui/components/icon"; + +import type { PublicEvent } from "../../types"; +import { EventDetailBlocks } from "../../types"; +import { EventDuration } from "./Duration"; +import { EventLocations } from "./Locations"; +import { EventOccurences } from "./Occurences"; +import { EventPrice } from "./Price"; + +type EventDetailsPropsBase = { + event: PublicEvent; + className?: string; +}; + +type EventDetailDefaultBlock = { + blocks?: EventDetailBlocks[]; +}; + +// Rendering a custom block requires passing a name prop, +// which is used as a key for the block. +type EventDetailCustomBlock = { + blocks?: React.FC[]; + name: string; +}; + +type EventDetailsProps = EventDetailsPropsBase & (EventDetailDefaultBlock | EventDetailCustomBlock); + +interface EventMetaProps { + icon: React.FC<{ className: string }> | string; + children: React.ReactNode; + // Emphasises the text in the block. For now only + // applying in dark mode. + highlight?: boolean; + contentClassName?: string; + className?: string; +} + +/** + * Default order in which the event details will be rendered. + */ +const defaultEventDetailsBlocks = [ + EventDetailBlocks.DESCRIPTION, + EventDetailBlocks.REQUIRES_CONFIRMATION, + EventDetailBlocks.DURATION, + EventDetailBlocks.OCCURENCES, + EventDetailBlocks.LOCATION, + EventDetailBlocks.PRICE, +]; + +/** + * Helper component that ensures the meta data of an event is + * rendered in a consistent way — adds an icon and children (text usually). + */ +export const EventMetaBlock = ({ + icon: Icon, + children, + highlight, + contentClassName, + className, +}: EventMetaProps) => { + if (!React.Children.count(children)) return null; + + return ( + <div + className={classNames( + "flex items-start justify-start text-sm", + highlight ? "text-emphasis" : "text-text", + className + )}> + {typeof Icon === "string" ? ( + <img + src={Icon} + alt="" + // @TODO: Use SVG's instead of images, so we can get rid of the filter. + className="mr-2 mt-[2px] h-4 w-4 flex-shrink-0 [filter:invert(0.5)_brightness(0.5)] dark:[filter:invert(1)_brightness(0.9)]" + /> + ) : ( + <Icon className="relative z-20 mr-2 mt-[2px] h-4 w-4 flex-shrink-0" /> + )} + <div className={classNames("relative z-10", contentClassName)}>{children}</div> + </div> + ); +}; + +/** + * Component that renders event meta data in a structured way, with icons and labels. + * The component can be configured to show only specific blocks by overriding the + * `blocks` prop. The blocks prop takes in an array of block names, defined + * in the `EventDetailBlocks` enum. See the `defaultEventDetailsBlocks` const + * for the default order in which the blocks will be rendered. + * + * As part of the blocks array you can also decide to render a custom React Component, + * which will then also be rendered. + * + * Example: + * const MyCustomBlock = () => <div>Something nice</div>; + * <EventDetails event={event} blocks={[EventDetailBlocks.LOCATION, MyCustomBlock]} /> + */ +export const EventDetails = ({ event, blocks = defaultEventDetailsBlocks }: EventDetailsProps) => { + const { t } = useLocale(); + + return ( + <> + {blocks.map((block) => { + if (typeof block === "function") { + return <Fragment key={block.name}>{block(event)}</Fragment>; + } + + switch (block) { + case EventDetailBlocks.DESCRIPTION: + if (!event.description) return null; + return ( + <EventMetaBlock key={block} icon={Info} contentClassName="break-words max-w-full overflow-clip"> + <div dangerouslySetInnerHTML={{ __html: event.description }} /> + </EventMetaBlock> + ); + + case EventDetailBlocks.DURATION: + return ( + <EventMetaBlock key={block} icon={Clock}> + <EventDuration event={event} /> + </EventMetaBlock> + ); + + case EventDetailBlocks.LOCATION: + if (!event?.locations?.length) return null; + return ( + <React.Fragment key={block}> + <EventLocations event={event} /> + </React.Fragment> + ); + + case EventDetailBlocks.REQUIRES_CONFIRMATION: + if (!event.requiresConfirmation) return null; + + return ( + <EventMetaBlock key={block} icon={CheckSquare}> + {t("requires_confirmation")} + </EventMetaBlock> + ); + + case EventDetailBlocks.OCCURENCES: + if (!event.requiresConfirmation || !event.recurringEvent) return null; + + return ( + <EventMetaBlock key={block} icon={RefreshCcw}> + <EventOccurences event={event} /> + </EventMetaBlock> + ); + + case EventDetailBlocks.PRICE: + if (event.price === 0) return null; + + return ( + <EventMetaBlock key={block} icon={CreditCard}> + <EventPrice event={event} /> + </EventMetaBlock> + ); + } + })} + </> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Duration.tsx b/packages/features/bookings/components/event-meta/Duration.tsx new file mode 100644 index 0000000000..f3965a9b6f --- /dev/null +++ b/packages/features/bookings/components/event-meta/Duration.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; + +import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Badge } from "@calcom/ui"; + +import type { PublicEvent } from "../../types"; + +export const EventDuration = ({ event }: { event: PublicEvent }) => { + const { t } = useLocale(); + const [selectedDuration, setSelectedDuration] = useBookerStore((state) => [ + state.selectedDuration, + state.setSelectedDuration, + ]); + + // Sets initial value of selected duration to the default duration. + useEffect(() => { + // Only store event duration in url if event has multiple durations. + if (!selectedDuration && event.metadata?.multipleDuration) setSelectedDuration(event.length); + }, [selectedDuration, setSelectedDuration, event.length, event.metadata?.multipleDuration]); + + if (!event?.metadata?.multipleDuration) return <>{t("multiple_duration_mins", { count: event.length })}</>; + + return ( + <div className="flex flex-wrap gap-2"> + {event.metadata.multipleDuration.map((duration) => ( + <Badge + variant="gray" + className={classNames(selectedDuration === duration && "bg-inverted text-inverted")} + size="md" + key={duration} + onClick={() => setSelectedDuration(duration)}>{`${duration} ${t("minute_timeUnit")}`}</Badge> + ))} + </div> + ); +}; diff --git a/packages/features/bookings/components/event-meta/EventMeta.stories.mdx b/packages/features/bookings/components/event-meta/EventMeta.stories.mdx new file mode 100644 index 0000000000..d177723916 --- /dev/null +++ b/packages/features/bookings/components/event-meta/EventMeta.stories.mdx @@ -0,0 +1,42 @@ +import { Canvas, Meta, Story, ArgsTable } from '@storybook/addon-docs'; +import { Examples, Example, Note, Title, VariantsTable, VariantColumn, RowTitles, CustomArgsTable} from '@calcom/storybook/components' +import { Icon } from "@calcom/ui"; + +import { EventDetails } from './Details'; +import { EventTitle } from './Title'; +import { EventMembers } from './Members'; +import { mockEvent } from './event.mock.ts'; + +<Meta title="Features/Events/Meta" component={EventDetails} /> + +<Title title="Event Meta" suffix="Brief" subtitle="Version 2.0 — Last Update: 12 Dec 2022"/> + +<Examples title="Combined event meta block"> + <div style={{maxWidth: 300}}> + <Example title="Event Title"> + <EventTitle event={mockEvent}/> + </Example> + <Example title="Event Details"> + <EventDetails event={mockEvent}/> + </Example> + </div> +</Examples> + +<Canvas> + <Story name="All variants"> + <VariantsTable titles={['Event Meta Components']} columnMinWidth={150}> + <VariantRow variant=""> + <div style={{maxWidth: 300}}> + <EventMembers users={ + [ + {name: "Pro example", username: "pro"}, + {name: "Team example", username: "team"} + ] + } /> + <EventTitle>Quick catch-up</EventTitle> + <EventDetails event={mockEvent} /> + </div> + </VariantRow> + </VariantsTable> + </Story> +</Canvas> diff --git a/packages/features/bookings/components/event-meta/Locations.tsx b/packages/features/bookings/components/event-meta/Locations.tsx new file mode 100644 index 0000000000..2c5db9177b --- /dev/null +++ b/packages/features/bookings/components/event-meta/Locations.tsx @@ -0,0 +1,42 @@ +import { getEventLocationType } from "@calcom/app-store/locations"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Tooltip } from "@calcom/ui"; +import { MapPin } from "@calcom/ui/components/icon"; + +import type { PublicEvent } from "../../types"; +import { EventMetaBlock } from "./Details"; + +export const EventLocations = ({ event }: { event: PublicEvent }) => { + const { t } = useLocale(); + const locations = event.locations; + if (!locations?.length) return null; + + return ( + <EventMetaBlock icon={MapPin}> + {locations.length === 1 && ( + <div key={locations[0].type}>{t(getEventLocationType(locations[0].type)?.label ?? "")}</div> + )} + {locations.length > 1 && ( + <div + key={locations[0].type} + className="before:bg-subtle relative before:pointer-events-none before:absolute before:inset-0 before:left-[-30px] before:top-[-5px] before:bottom-[-5px] before:w-[calc(100%_+_35px)] before:rounded-md before:py-3 before:opacity-0 before:transition-opacity hover:before:opacity-100"> + <Tooltip + content={ + <> + <p className="mb-2">{t("select_on_next_step")}</p> + <ul className="list-disc pl-3"> + {locations.map((location) => ( + <li key={location.type}> + <span>{t(getEventLocationType(location.type)?.label ?? "")}</span> + </li> + ))} + </ul> + </> + }> + <span className="relative z-[2] py-2">{t("num_locations", { num: locations.length })}</span> + </Tooltip> + </div> + )} + </EventMetaBlock> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx new file mode 100644 index 0000000000..e416843322 --- /dev/null +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -0,0 +1,42 @@ +import { CAL_URL } from "@calcom/lib/constants"; +import { AvatarGroup } from "@calcom/ui"; + +import type { PublicEvent } from "../../types"; +import { SchedulingType } from ".prisma/client"; + +export interface EventMembersProps { + /** + * Used to determine whether all members should be shown or not. + * In case of Round Robin type, members aren't shown. + */ + schedulingType: PublicEvent["schedulingType"]; + users: PublicEvent["users"]; + profile: PublicEvent["profile"]; +} + +export const EventMembers = ({ schedulingType, users, profile }: EventMembersProps) => { + const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN; + const shownUsers = showMembers ? [...users, profile] : [profile]; + + const avatars = shownUsers + .map((user) => ({ + title: `${user.name}`, + image: "image" in user ? `${user.image}` : `${CAL_URL}/${user.username}/avatar.png`, + alt: user.name || undefined, + href: user.username ? `${CAL_URL}/${user.username}` : undefined, + })) + .filter((item) => !!item.image) + .filter((item, index, self) => self.findIndex((t) => t.image === item.image) === index); + + return ( + <> + <AvatarGroup size="sm" className="border-muted" items={avatars} /> + <p className="text-subtle text-sm"> + {users + .map((user) => user.name) + .filter((name) => name) + .join(", ")} + </p> + </> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Occurences.tsx b/packages/features/bookings/components/event-meta/Occurences.tsx new file mode 100644 index 0000000000..f7b9b72c18 --- /dev/null +++ b/packages/features/bookings/components/event-meta/Occurences.tsx @@ -0,0 +1,42 @@ +import { useEffect } from "react"; + +import { useBookerStore } from "@calcom/features/bookings/Booker/store"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { getRecurringFreq } from "@calcom/lib/recurringStrings"; +import { Input } from "@calcom/ui"; + +import type { PublicEvent } from "../../types"; + +export const EventOccurences = ({ event }: { event: PublicEvent }) => { + const { t } = useLocale(); + const [setRecurringEventCount, recurringEventCount] = useBookerStore((state) => [ + state.setRecurringEventCount, + state.recurringEventCount, + ]); + + // Set initial value in booker store. + useEffect(() => { + if (!event.recurringEvent?.count) return; + setRecurringEventCount(event.recurringEvent.count); + }, [setRecurringEventCount, event.recurringEvent]); + + if (!event.recurringEvent) return null; + + return ( + <> + {getRecurringFreq({ t, recurringEvent: event.recurringEvent })} + <br /> + <Input + className="my-1 mr-3 inline-flex h-[26px] w-[46px] py-0 px-1" + type="number" + defaultValue={event.recurringEvent.count} + onChange={(event) => { + setRecurringEventCount(parseInt(event?.target.value)); + }} + /> + {t("occurrence", { + count: recurringEventCount || event.recurringEvent.count, + })} + </> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Price.tsx b/packages/features/bookings/components/event-meta/Price.tsx new file mode 100644 index 0000000000..792e88a444 --- /dev/null +++ b/packages/features/bookings/components/event-meta/Price.tsx @@ -0,0 +1,18 @@ +import getPaymentAppData from "@calcom/lib/getPaymentAppData"; + +import type { PublicEvent } from "../../types"; + +export const EventPrice = ({ event }: { event: PublicEvent }) => { + const stripeAppData = getPaymentAppData(event); + + if (stripeAppData.price === 0) return null; + + return ( + <> + {Intl.NumberFormat("en", { + style: "currency", + currency: stripeAppData.currency.toUpperCase(), + }).format(stripeAppData.price / 100.0)} + </> + ); +}; diff --git a/packages/features/bookings/components/event-meta/Skeleton.tsx b/packages/features/bookings/components/event-meta/Skeleton.tsx new file mode 100644 index 0000000000..cc7f73d255 --- /dev/null +++ b/packages/features/bookings/components/event-meta/Skeleton.tsx @@ -0,0 +1,19 @@ +import classNames from "@calcom/lib/classNames"; +import { SkeletonText } from "@calcom/ui"; + +export const EventMetaSkeleton = () => ( + <div className="flex flex-col"> + <SkeletonText className="h-6 w-6 rounded-full" /> + <SkeletonText className="mt-2 h-5 w-32" /> + <SkeletonText className="mt-2 h-8 w-48" /> + + <div className="mt-8"> + {Array.from({ length: 4 }).map((_, i) => ( + <div className="mb-2 flex flex-row items-center" key={i}> + <SkeletonText className="mr-3 h-5 w-5 rounded-full" /> + <SkeletonText className={classNames("h-6", i > 1 ? "w-24" : "w-32")} /> + </div> + ))} + </div> + </div> +); diff --git a/packages/features/bookings/components/event-meta/Title.tsx b/packages/features/bookings/components/event-meta/Title.tsx new file mode 100644 index 0000000000..a147b8cfc2 --- /dev/null +++ b/packages/features/bookings/components/event-meta/Title.tsx @@ -0,0 +1,15 @@ +import classNames from "@calcom/lib/classNames"; + +interface EventTitleProps { + children: React.ReactNode; + /** + * Option to override the default h1 tag. + */ + as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span"; + className?: string; +} + +export const EventTitle = ({ children, as, className }: EventTitleProps) => { + const El = as || "h1"; + return <El className={classNames("text-text text-xl font-semibold", className)}>{children}</El>; +}; diff --git a/packages/features/bookings/components/event-meta/event.mock.ts b/packages/features/bookings/components/event-meta/event.mock.ts new file mode 100644 index 0000000000..1b9e0cad9d --- /dev/null +++ b/packages/features/bookings/components/event-meta/event.mock.ts @@ -0,0 +1,14 @@ +import { RouterOutputs } from "@calcom/trpc/react"; + +export const mockEvent: RouterOutputs["viewer"]["public"]["event"] = { + id: 1, + title: "Quick check-in", + slug: "quick-check-in", + eventName: "Quick check-in", + description: + "Use this event for a quick 15 minute catchup. Visit this long url to test the component https://cal.com/averylongurlwithoutspacesthatshouldntbreaklayout", + users: [{ name: "Pro Example", username: "pro" }], + schedulingType: null, + length: 30, + locations: [{ type: "integrations:google:meet" }, { type: "integrations:zoom" }], +}; diff --git a/packages/features/bookings/components/event-meta/index.ts b/packages/features/bookings/components/event-meta/index.ts new file mode 100644 index 0000000000..ca610b80d5 --- /dev/null +++ b/packages/features/bookings/components/event-meta/index.ts @@ -0,0 +1,4 @@ +export { EventDetails, EventMetaBlock } from "./Details"; +export { EventTitle } from "./Title"; +export { EventMetaSkeleton } from "./Skeleton"; +export { EventMembers } from "./Members"; diff --git a/packages/features/bookings/index.ts b/packages/features/bookings/index.ts new file mode 100644 index 0000000000..75d6c92b0e --- /dev/null +++ b/packages/features/bookings/index.ts @@ -0,0 +1,8 @@ +export { + EventDetails, + EventMembers, + EventMetaBlock, + EventMetaSkeleton, + EventTitle, +} from "./components/event-meta"; +export { AvailableTimes, AvailableTimesSkeleton } from "./components/AvailableTimes"; diff --git a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx new file mode 100644 index 0000000000..9cf82e7b1f --- /dev/null +++ b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx @@ -0,0 +1,83 @@ +import { v4 as uuidv4 } from "uuid"; + +import dayjs from "@calcom/dayjs"; +import { parseRecurringDates } from "@calcom/lib/parse-dates"; + +import type { PublicEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../types"; + +type BookingOptions = { + values: Record<string, unknown>; + event: PublicEvent; + date: string; + // @NOTE: duration is not validated in this function + duration: number | undefined | null; + timeZone: string; + language: string; + rescheduleUid: string | undefined; + username: string; + metadata?: Record<string, string>; +}; + +export const mapBookingToMutationInput = ({ + values, + event, + date, + duration, + timeZone, + language, + rescheduleUid, + username, + metadata, +}: BookingOptions): BookingCreateBody => { + return { + ...values, + user: username, + start: dayjs(date).format(), + end: dayjs(date) + // Defaults to the default event length in case no custom duration is set. + .add(duration || event.length, "minute") + .format(), + eventTypeId: event.id, + eventTypeSlug: event.slug, + timeZone: timeZone, + language: language, + rescheduleUid, + metadata: metadata || {}, + hasHashedBookingLink: false, + // hasHashedBookingLink, + // hashedLink, + }; +}; + +// This method is here to ensure that the types are correct (recurring count is required), +// as well as generate a unique ID for the recurring bookings and turn one single booking +// into an array of mutiple bookings based on the recurring count. +// Other than that it forwards the mapping to mapBookingToMutationInput. +export const mapRecurringBookingToMutationInput = ( + booking: BookingOptions, + recurringCount: number +): RecurringBookingCreateBody[] => { + const recurringEventId = uuidv4(); + const [, recurringDates] = parseRecurringDates( + { + startDate: booking.date, + timeZone: booking.timeZone, + recurringEvent: booking.event.recurringEvent, + recurringCount, + withDefaultTimeFormat: true, + }, + booking.language + ); + + const input = mapBookingToMutationInput(booking); + + return recurringDates.map((recurringDate) => ({ + ...input, + start: dayjs(recurringDate).format(), + end: dayjs(recurringDate) + .add(booking.duration || booking.event.length, "minute") + .format(), + recurringEventId, + recurringCount: recurringDates.length, + })); +}; diff --git a/packages/features/bookings/lib/create-booking.ts b/packages/features/bookings/lib/create-booking.ts new file mode 100644 index 0000000000..e7931723c3 --- /dev/null +++ b/packages/features/bookings/lib/create-booking.ts @@ -0,0 +1,8 @@ +import { post } from "@calcom/lib/fetch-wrapper"; + +import type { BookingCreateBody, BookingResponse } from "../types"; + +export const createBooking = async (data: BookingCreateBody) => { + const response = await post<BookingCreateBody, BookingResponse>("/api/book/event", data); + return response; +}; diff --git a/apps/web/lib/mutations/bookings/create-recurring-booking.ts b/packages/features/bookings/lib/create-recurring-booking.ts similarity index 67% rename from apps/web/lib/mutations/bookings/create-recurring-booking.ts rename to packages/features/bookings/lib/create-recurring-booking.ts index 8cec5b82b4..41e29c467f 100644 --- a/apps/web/lib/mutations/bookings/create-recurring-booking.ts +++ b/packages/features/bookings/lib/create-recurring-booking.ts @@ -1,18 +1,10 @@ -import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; +import * as fetch from "@calcom/lib/fetch-wrapper"; import type { AppsStatus } from "@calcom/types/Calendar"; -import * as fetch from "@lib/core/http/fetch-wrapper"; -import type { BookingResponse } from "@lib/types/booking"; +import type { RecurringBookingCreateBody, BookingResponse } from "../types"; -type ExtendedBookingCreateBody = BookingCreateBody & { - noEmail?: boolean; - recurringCount?: number; - appsStatus?: AppsStatus[] | undefined; - allRecurringDates?: string[]; - currentRecurringIndex?: number; -}; - -const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => { +// @TODO: Didn't look at the contents of this function in order to not break old booking page. +export const createRecurringBooking = async (data: RecurringBookingCreateBody[]) => { const createdBookings: BookingResponse[] = []; const allRecurringDates: string[] = data.map((booking) => booking.start); let appsStatus: AppsStatus[] | undefined = undefined; @@ -35,7 +27,7 @@ const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => { appsStatus = Object.values(calcAppsStatus); } - const response = await fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", { + const response = await fetch.post<RecurringBookingCreateBody, BookingResponse>("/api/book/event", { ...booking, appsStatus, allRecurringDates, @@ -46,5 +38,3 @@ const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => { } return createdBookings; }; - -export default createRecurringBooking; diff --git a/packages/features/bookings/lib/get-booking.ts b/packages/features/bookings/lib/get-booking.ts new file mode 100644 index 0000000000..40ef06c938 --- /dev/null +++ b/packages/features/bookings/lib/get-booking.ts @@ -0,0 +1,163 @@ +import type { Prisma, PrismaClient } from "@prisma/client"; +import type { z } from "zod"; + +import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; +import slugify from "@calcom/lib/slugify"; +import prisma from "@calcom/prisma"; + +type BookingSelect = { + description: true; + customInputs: true; + attendees: { + select: { + email: true; + name: true; + }; + }; + location: true; +}; + +// Backward Compatibility for booking created before we had managed booking questions +function getResponsesFromOldBooking( + rawBooking: Prisma.BookingGetPayload<{ + select: BookingSelect; + }> +) { + const customInputs = rawBooking.customInputs || {}; + const responses = Object.keys(customInputs).reduce((acc, label) => { + acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs]; + return acc; + }, {}); + return { + // It is possible to have no attendees in a booking when the booking is cancelled. + name: rawBooking.attendees[0]?.name || "Nameless", + email: rawBooking.attendees[0]?.email || "", + guests: rawBooking.attendees.slice(1).map((attendee) => { + return attendee.email; + }), + notes: rawBooking.description || "", + location: { + value: rawBooking.location || "", + optionValue: rawBooking.location || "", + }, + ...responses, + }; +} + +async function getBooking(prisma: PrismaClient, uid: string) { + const rawBooking = await prisma.booking.findFirst({ + where: { + uid, + }, + select: { + id: true, + uid: true, + startTime: true, + description: true, + customInputs: true, + responses: true, + smsReminderNumber: true, + location: true, + attendees: { + select: { + email: true, + name: true, + bookingSeat: true, + }, + }, + user: { + select: { + id: true, + }, + }, + }, + }); + + if (!rawBooking) { + return rawBooking; + } + + const booking = getBookingWithResponses(rawBooking); + + if (booking) { + // @NOTE: had to do this because Server side cant return [Object objects] + // probably fixable with json.stringify -> json.parse + booking["startTime"] = (booking?.startTime as Date)?.toISOString() as unknown as Date; + } + + return booking; +} + +export type GetBookingType = Prisma.PromiseReturnType<typeof getBooking>; + +export const getBookingWithResponses = < + T extends Prisma.BookingGetPayload<{ + select: BookingSelect & { + responses: true; + }; + }> +>( + booking: T +) => { + return { + ...booking, + responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)), + } as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> }; +}; + +export default getBooking; + +export const getBookingByUidOrRescheduleUid = async (uid: string) => { + let eventTypeId: number | null = null; + let rescheduleUid: string | null = null; + eventTypeId = + ( + await prisma.booking.findFirst({ + where: { + uid, + }, + select: { + eventTypeId: true, + }, + }) + )?.eventTypeId || null; + + // If no booking is found via the uid, it's probably a booking seat, + // which we query next. + let attendeeEmail: string | null = null; + if (!eventTypeId) { + const bookingSeat = await prisma.bookingSeat.findFirst({ + where: { + referenceUid: uid, + }, + select: { + id: true, + attendee: true, + booking: { + select: { + uid: true, + }, + }, + }, + }); + if (bookingSeat) { + rescheduleUid = bookingSeat.booking.uid; + attendeeEmail = bookingSeat.attendee.email; + } + } + + // If we don't have a booking and no rescheduleUid, the ID is invalid, + // and we return null here. + if (!eventTypeId && !rescheduleUid) return null; + + const booking = await getBooking(prisma, rescheduleUid || uid); + + if (!booking) return null; + + return { + ...booking, + attendees: rescheduleUid + ? booking.attendees.filter((attendee) => attendee.email === attendeeEmail) + : booking.attendees, + }; +}; diff --git a/packages/features/bookings/lib/index.ts b/packages/features/bookings/lib/index.ts new file mode 100644 index 0000000000..a9a36c1264 --- /dev/null +++ b/packages/features/bookings/lib/index.ts @@ -0,0 +1,7 @@ +export { useTimePreferences, timePreferencesStore } from "./timePreferences"; +export { + mapBookingToMutationInput, + mapRecurringBookingToMutationInput, +} from "./book-event-form/booking-to-mutation-input-mapper"; +export { createBooking } from "./create-booking"; +export { createRecurringBooking } from "./create-recurring-booking"; diff --git a/packages/features/bookings/lib/timePreferences.ts b/packages/features/bookings/lib/timePreferences.ts new file mode 100644 index 0000000000..4e56f1bf16 --- /dev/null +++ b/packages/features/bookings/lib/timePreferences.ts @@ -0,0 +1,34 @@ +import { create } from "zustand"; + +import dayjs from "@calcom/dayjs"; +import { TimeFormat, detectBrowserTimeFormat, setIs24hClockInLocalStorage } from "@calcom/lib/timeFormat"; +import { localStorage } from "@calcom/lib/webstorage"; + +type TimePreferencesStore = { + timeFormat: TimeFormat.TWELVE_HOUR | TimeFormat.TWENTY_FOUR_HOUR; + setTimeFormat: (format: TimeFormat.TWELVE_HOUR | TimeFormat.TWENTY_FOUR_HOUR) => void; + timezone: string; + setTimezone: (timeZone: string) => void; +}; + +const timezoneLocalStorageKey = "timeOption.preferredTimeZone"; + +/** + * This hook is NOT inside the user feature, since + * these settings only apply to the booker component. They will not reflect + * any changes made in the user settings. + */ +export const timePreferencesStore = create<TimePreferencesStore>((set) => ({ + timeFormat: detectBrowserTimeFormat, + setTimeFormat: (format: TimeFormat.TWELVE_HOUR | TimeFormat.TWENTY_FOUR_HOUR) => { + setIs24hClockInLocalStorage(format === TimeFormat.TWENTY_FOUR_HOUR); + set({ timeFormat: format }); + }, + timezone: localStorage.getItem(timezoneLocalStorageKey) || dayjs.tz.guess(), + setTimezone: (timezone: string) => { + localStorage.setItem(timezoneLocalStorageKey, timezone); + set({ timezone }); + }, +})); + +export const useTimePreferences = timePreferencesStore; diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts new file mode 100644 index 0000000000..6415612cd0 --- /dev/null +++ b/packages/features/bookings/types.ts @@ -0,0 +1,33 @@ +import type { ErrorOption, FieldPath } from "react-hook-form"; + +import type { BookingCreateBody } from "@calcom/prisma/zod-utils"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import type { AppsStatus } from "@calcom/types/Calendar"; + +export type PublicEvent = NonNullable<RouterOutputs["viewer"]["public"]["event"]>; +export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[]; + +export enum EventDetailBlocks { + DESCRIPTION, + // Includes duration select when event has multiple durations. + DURATION, + LOCATION, + REQUIRES_CONFIRMATION, + // Includes input to select # of occurences. + OCCURENCES, + PRICE, +} + +export type { BookingCreateBody }; + +export type RecurringBookingCreateBody = BookingCreateBody & { + noEmail?: boolean; + recurringCount?: number; + appsStatus?: AppsStatus[] | undefined; + allRecurringDates?: string[]; + currentRecurringIndex?: number; +}; + +export type BookingResponse = Awaited< + ReturnType<typeof import("@calcom/features/bookings/lib/handleNewBooking").default> +>; diff --git a/packages/features/eventtypes/components/CheckedUserSelect.tsx b/packages/features/eventtypes/components/CheckedUserSelect.tsx index db3fcbfb59..d1c188f003 100644 --- a/packages/features/eventtypes/components/CheckedUserSelect.tsx +++ b/packages/features/eventtypes/components/CheckedUserSelect.tsx @@ -1,10 +1,9 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { Props } from "react-select"; -import { CAL_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Avatar, EmptyScreen, Label, Select } from "@calcom/ui"; -import { FiUserPlus, FiX } from "@calcom/ui/components/icon"; +import { UserPlus, X } from "@calcom/ui/components/icon"; export type CheckedUserSelectOption = { avatar: string; @@ -48,14 +47,13 @@ export const CheckedUserSelect = ({ <div className="flex overflow-hidden rounded-md border border-gray-200 bg-white"> <ul className="w-full" data-testid="managed-event-types" ref={animationRef}> {value.map((option, index) => { - const calLink = `${CAL_URL}/${option.value}`; return ( <li key={option.value} className={`flex py-2 px-3 ${index === value.length - 1 ? "" : "border-b"}`}> <Avatar size="sm" imageSrc={option.avatar} alt={option.label} /> <p className="my-auto ml-3 text-sm text-gray-900">{option.label}</p> - <FiX + <X onClick={() => props.onChange(value.filter((item) => item.value !== option.value))} className="my-auto ml-auto" /> @@ -68,7 +66,7 @@ export const CheckedUserSelect = ({ ) : ( <div className="mt-6"> <EmptyScreen - Icon={FiUserPlus} + Icon={UserPlus} headline={t("no_assigned_members")} description={t("start_assigning_members_above")} /> diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts new file mode 100644 index 0000000000..a29b4e41b1 --- /dev/null +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -0,0 +1,201 @@ +import type { User } from "@prisma/client"; +import { Prisma } from "@prisma/client"; + +import type { LocationObject } from "@calcom/app-store/locations"; +import { privacyFilteredLocations } from "@calcom/app-store/locations"; +import { getAppFromSlug } from "@calcom/app-store/utils"; +import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; +import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; +import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import type { PrismaClient } from "@calcom/prisma/client"; +import { + EventTypeMetaDataSchema, + customInputSchema, + userMetadata as userMetadataSchema, +} from "@calcom/prisma/zod-utils"; + +const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({ + id: true, + title: true, + description: true, + eventName: true, + slug: true, + schedulingType: true, + length: true, + locations: true, + customInputs: true, + disableGuests: true, + // @TODO: Could this contain sensitive data? + metadata: true, + requiresConfirmation: true, + recurringEvent: true, + price: true, + currency: true, + seatsPerTimeSlot: true, + bookingFields: true, + team: true, + workflows: { + include: { + workflow: { + include: { + steps: true, + }, + }, + }, + }, + hosts: { + select: { + user: { + select: { + username: true, + name: true, + weekStart: true, + brandColor: true, + darkBrandColor: true, + }, + }, + }, + }, + owner: true, +}); + +export const getPublicEvent = async (username: string, eventSlug: string, prisma: PrismaClient) => { + const usernameList = username.split("+"); + + // In case of dynamic group event, we fetch user's data and use the default event. + if (usernameList.length > 1) { + const users = await prisma.user.findMany({ + where: { + username: { + in: usernameList, + }, + }, + select: { + username: true, + name: true, + weekStart: true, + metadata: true, + brandColor: true, + darkBrandColor: true, + }, + }); + + const defaultEvent = getDefaultEvent(eventSlug); + let locations = defaultEvent.locations ? (defaultEvent.locations as LocationObject[]) : []; + + // Get the prefered location type from the first user + const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata || {}); + const preferedLocationType = firstUsersMetadata?.defaultConferencingApp; + + if (preferedLocationType?.appSlug) { + const foundApp = getAppFromSlug(preferedLocationType.appSlug); + const appType = foundApp?.appData?.location?.type; + if (appType) { + // Replace the location with the prefered location type + // This will still be default to daily if the app is not found + locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[]; + } + } + + return { + ...defaultEvent, + bookingFields: getBookingFieldsWithSystemFields(defaultEvent), + // Clears meta data since we don't want to send this in the public api. + users: users.map((user) => ({ ...user, metadata: undefined })), + locations: privacyFilteredLocations(locations), + profile: { + username: users[0].username, + name: users[0].name, + weekStart: users[0].weekStart, + image: `${WEBAPP_URL}/${users[0].username}/avatar.png`, + brandColor: users[0].brandColor, + darkBrandColor: users[0].darkBrandColor, + }, + }; + } + + // In case it's not a group event, it's either a single user or a team, and we query that data. + const event = await prisma.eventType.findFirst({ + where: { + slug: eventSlug, + OR: [ + { + users: { + some: { + username, + }, + }, + }, + { + team: { + slug: username, + }, + }, + ], + }, + select: publicEventSelect, + }); + + if (!event) return null; + + return { + ...event, + description: markdownToSafeHTML(event.description), + metadata: EventTypeMetaDataSchema.parse(event.metadata || {}), + customInputs: customInputSchema.array().parse(event.customInputs || []), + locations: privacyFilteredLocations((event.locations || []) as LocationObject[]), + bookingFields: getBookingFieldsWithSystemFields(event), + recurringEvent: isRecurringEvent(event.recurringEvent) ? parseRecurringEvent(event.recurringEvent) : null, + // Sets user data on profile object for easier access + profile: getProfileFromEvent(event), + users: getUsersFromEvent(event), + }; +}; + +const eventData = Prisma.validator<Prisma.EventTypeArgs>()({ + select: publicEventSelect, +}); + +type Event = Prisma.EventTypeGetPayload<typeof eventData>; + +function getProfileFromEvent(event: Event) { + const { team, hosts, owner } = event; + const profile = team || hosts?.[0]?.user || owner; + if (!profile) throw new Error("Event has no owner"); + + const username = "username" in profile ? profile.username : team?.slug; + if (!username) throw new Error("Event has no username/team slug"); + const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday"; + const basePath = team ? `/team/${username}` : `/${username}`; + + return { + username, + name: profile.name, + weekStart, + image: `${WEBAPP_URL}${basePath}/avatar.png`, + brandColor: profile.brandColor, + darkBrandColor: profile.darkBrandColor, + }; +} + +function getUsersFromEvent(event: Event) { + const { team, hosts, owner } = event; + if (team) { + return (hosts || []).map(mapHostsToUsers); + } + + if (!owner) throw new Error("Event has no owner"); + + const { username, name, weekStart } = owner; + return [{ username, name, weekStart }]; +} + +function mapHostsToUsers(host: { user: Pick<User, "username" | "name" | "weekStart"> }) { + return { + username: host.user.username, + name: host.user.name, + weekStart: host.user.weekStart, + }; +} diff --git a/packages/features/package.json b/packages/features/package.json index f1c05ccb33..7582de6295 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -12,7 +12,13 @@ "@calcom/ui": "*", "@lexical/react": "^0.5.0", "dompurify": "^2.4.1", + "framer-motion": "^10.12.3", "lexical": "^0.5.0", - "zustand": "^4.1.4" + "react-sticky-box": "^2.0.4", + "zustand": "^4.3.2" + }, + "devDependencies": { + "@testing-library/react-hooks": "^8.0.1", + "mockdate": "^3.0.5" } } diff --git a/packages/features/schedules/index.ts b/packages/features/schedules/index.ts index 40b494c5f8..fe06d0e457 100644 --- a/packages/features/schedules/index.ts +++ b/packages/features/schedules/index.ts @@ -1 +1,3 @@ export * from "./components"; +export type { Slots } from "./lib/use-schedule"; +export { useSchedule, useSlotsForDate, useNonEmptyScheduleDays } from "./lib/use-schedule"; diff --git a/packages/features/schedules/lib/use-schedule/index.ts b/packages/features/schedules/lib/use-schedule/index.ts new file mode 100644 index 0000000000..ce81319e9c --- /dev/null +++ b/packages/features/schedules/lib/use-schedule/index.ts @@ -0,0 +1,4 @@ +export { useSchedule } from "./useSchedule"; +export { useSlotsForDate } from "./useSlotsForDate"; +export { useNonEmptyScheduleDays } from "./useNonEmptyScheduleDays"; +export type { Slots } from "./types"; diff --git a/packages/features/schedules/lib/use-schedule/types.ts b/packages/features/schedules/lib/use-schedule/types.ts new file mode 100644 index 0000000000..72ba9df508 --- /dev/null +++ b/packages/features/schedules/lib/use-schedule/types.ts @@ -0,0 +1,3 @@ +import { RouterOutputs } from "@calcom/trpc/react"; + +export type Slots = RouterOutputs["viewer"]["public"]["slots"]["getSchedule"]["slots"]; diff --git a/packages/features/schedules/lib/use-schedule/useNonEmptyScheduleDays.ts b/packages/features/schedules/lib/use-schedule/useNonEmptyScheduleDays.ts new file mode 100644 index 0000000000..eca607c322 --- /dev/null +++ b/packages/features/schedules/lib/use-schedule/useNonEmptyScheduleDays.ts @@ -0,0 +1,14 @@ +import { useMemo } from "react"; + +import type { Slots } from "../use-schedule"; + +export const getNonEmptyScheduleDays = (slots?: Slots) => { + if (typeof slots === "undefined") return []; + return Object.keys(slots).filter((day) => slots[day].length > 0); +}; + +export const useNonEmptyScheduleDays = (slots?: Slots) => { + const days = useMemo(() => getNonEmptyScheduleDays(slots), [slots]); + + return days; +}; diff --git a/packages/features/schedules/lib/use-schedule/useSchedule.ts b/packages/features/schedules/lib/use-schedule/useSchedule.ts new file mode 100644 index 0000000000..40e1ad2028 --- /dev/null +++ b/packages/features/schedules/lib/use-schedule/useSchedule.ts @@ -0,0 +1,49 @@ +import dayjs from "@calcom/dayjs"; +import { trpc } from "@calcom/trpc/react"; + +type UseScheduleWithCacheArgs = { + username?: string | null; + eventSlug?: string | null; + eventId?: number | null; + month?: string | null; + timezone?: string | null; + prefetchNextMonth?: boolean; +}; + +export const useSchedule = ({ + month, + timezone, + username, + eventSlug, + eventId, + prefetchNextMonth, +}: UseScheduleWithCacheArgs) => { + const monthDayjs = month ? dayjs(month) : dayjs(); + const nextMonthDayjs = monthDayjs.add(1, "month"); + + // Why the non-null assertions? All of these arguments are checked in the enabled condition, + // and the query will not run if they are null. However, the check in `enabled` does + // no satisfy typscript. + return trpc.viewer.public.slots.getSchedule.useQuery( + { + usernameList: username && username.indexOf("+") > -1 ? username.split("+") : [username!], + eventTypeSlug: eventSlug!, + // @TODO: Old code fetched 2 days ago if we were fetching the current month. + // Do we want / need to keep that behavior? + startTime: monthDayjs.startOf("month").toISOString(), + // if `prefetchNextMonth` is true, two months are fetched at once. + endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(), + timeZone: timezone!, + eventTypeId: eventId!, + }, + { + refetchOnWindowFocus: false, + enabled: + Boolean(username) && + Boolean(eventSlug) && + (Boolean(eventId) || eventId === 0) && + Boolean(month) && + Boolean(timezone), + } + ); +}; diff --git a/packages/features/schedules/lib/use-schedule/useSlotsForDate.ts b/packages/features/schedules/lib/use-schedule/useSlotsForDate.ts new file mode 100644 index 0000000000..b74ede7ee6 --- /dev/null +++ b/packages/features/schedules/lib/use-schedule/useSlotsForDate.ts @@ -0,0 +1,31 @@ +import { useMemo } from "react"; + +import type { Slots } from "./types"; + +/** + * Get's slots for a specific date from the schedul cache. + * @param date Format YYYY-MM-DD + * @param scheduleCache Instance of useScheduleWithCache + */ +export const useSlotsForDate = (date: string | null, slots?: Slots) => { + const slotsForDate = useMemo(() => { + if (!date || typeof slots === "undefined") return []; + return slots[date] || []; + }, [date, slots]); + + return slotsForDate; +}; + +export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots) => { + const slotsForDates = useMemo(() => { + if (typeof slots === "undefined") return []; + return dates + .filter((date) => date !== null) + .map((date) => ({ + slots: slots[`${date}`] || [], + date, + })); + }, [dates, slots]); + + return slotsForDates; +}; diff --git a/packages/features/tsconfig.json b/packages/features/tsconfig.json index b8fb2a6430..baae362e9a 100644 --- a/packages/features/tsconfig.json +++ b/packages/features/tsconfig.json @@ -5,7 +5,8 @@ "paths": { "~/*": ["/*"] }, - "resolveJsonModule": true + "resolveJsonModule": true, + "esModuleInterop": true }, "include": [".", "../types/next-auth.d.ts"], "exclude": ["dist", "build", "node_modules"] diff --git a/packages/lib/array.ts b/packages/lib/array.ts new file mode 100644 index 0000000000..03d7c22932 --- /dev/null +++ b/packages/lib/array.ts @@ -0,0 +1 @@ +export const notUndefined = <T>(val: T | undefined): val is T => Boolean(val); diff --git a/packages/lib/date-fns/index.ts b/packages/lib/date-fns/index.ts index a6e12f1100..9f92f7b7dc 100644 --- a/packages/lib/date-fns/index.ts +++ b/packages/lib/date-fns/index.ts @@ -130,6 +130,21 @@ export const isNextDayInTimezone = (time: string, timezoneA: string, timezoneB: return hoursTimezoneBIsEarlier && timezoneBIsLaterTimezone; }; +const weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] as const; +type WeekDays = (typeof weekDays)[number]; +type WeekDayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6; + +/** + * Turns weekday string (eg "Monday") into a number (eg 1). + * Also accepts a number as parameter (and straight returns that), and accepts + * undefined as a parameter; returns 0 in that case. + */ +export const weekdayToWeekIndex = (weekday: WeekDays | string | number | undefined) => { + if (typeof weekday === "undefined") return 0; + if (typeof weekday === "number") return weekday >= 0 && weekday >= 6 ? (weekday as WeekDayIndex) : 0; + return (weekDays.indexOf(weekday as WeekDays) as WeekDayIndex) || 0; +}; + /** * Dayjs does not expose the timeZone value publicly through .get("timeZone") * instead, we as devs are required to somewhat hack our way to get the diff --git a/apps/web/lib/core/http/fetch-wrapper.ts b/packages/lib/fetch-wrapper.ts similarity index 97% rename from apps/web/lib/core/http/fetch-wrapper.ts rename to packages/lib/fetch-wrapper.ts index 4d5d6365e8..9c7dceb358 100644 --- a/apps/web/lib/core/http/fetch-wrapper.ts +++ b/packages/lib/fetch-wrapper.ts @@ -1,4 +1,4 @@ -import { HttpError } from "@lib/core/http/error"; +import { HttpError } from "./http-error"; async function http<T>(path: string, config: RequestInit): Promise<T> { const request = new Request(path, config); diff --git a/apps/web/lib/parseDate.ts b/packages/lib/parse-dates.ts similarity index 62% rename from apps/web/lib/parseDate.ts rename to packages/lib/parse-dates.ts index b262b5569e..d61d2d59d5 100644 --- a/apps/web/lib/parseDate.ts +++ b/packages/lib/parse-dates.ts @@ -1,24 +1,28 @@ -import type { I18n } from "next-i18next"; import { RRule } from "rrule"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; -import type { TimeFormat } from "@calcom/lib/timeFormat"; -import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat"; +import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat"; import type { RecurringEvent } from "@calcom/types/Calendar"; -import { parseZone } from "./parseZone"; +import { parseZone } from "./parse-zone"; -const processDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => { +type ExtraOptions = { withDefaultTimeFormat?: boolean; selectedTimeFormat?: TimeFormat }; + +const processDate = (date: string | null | Dayjs, language: string, options?: ExtraOptions) => { const parsedZone = parseZone(date); if (!parsedZone?.isValid()) return "Invalid date"; - const formattedTime = parsedZone?.format(selectedTimeFormat || detectBrowserTimeFormat); - return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" }); + const formattedTime = parsedZone?.format( + options?.withDefaultTimeFormat + ? TimeFormat.TWELVE_HOUR + : options?.selectedTimeFormat || detectBrowserTimeFormat + ); + return formattedTime + ", " + dayjs(date).toDate().toLocaleString(language, { dateStyle: "full" }); }; -export const parseDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => { +export const parseDate = (date: string | null | Dayjs, language: string, options?: ExtraOptions) => { if (!date) return ["No date"]; - return processDate(date, i18n, selectedTimeFormat); + return processDate(date, language, options); }; export const parseRecurringDates = ( @@ -28,14 +32,16 @@ export const parseRecurringDates = ( recurringEvent, recurringCount, selectedTimeFormat, + withDefaultTimeFormat, }: { startDate: string | null | Dayjs; timeZone?: string; recurringEvent: RecurringEvent | null; recurringCount: number; selectedTimeFormat?: TimeFormat; + withDefaultTimeFormat?: boolean; }, - i18n: I18n + language: string ): [string[], Date[]] => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { count, ...restRecurringEvent } = recurringEvent || {}; @@ -53,7 +59,7 @@ export const parseRecurringDates = ( }); const dateStrings = times.map((t) => { // finally; show in local timeZone again - return processDate(t.tz(timeZone), i18n, selectedTimeFormat); + return processDate(t.tz(timeZone), language, { selectedTimeFormat, withDefaultTimeFormat }); }); return [dateStrings, times.map((t) => t.toDate())]; diff --git a/apps/web/lib/parseZone.ts b/packages/lib/parse-zone.ts similarity index 100% rename from apps/web/lib/parseZone.ts rename to packages/lib/parse-zone.ts diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index b2468cc89a..8ac94e6b4f 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -25,6 +25,7 @@ import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml"; import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router"; +import { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent"; import { featureFlagRouter } from "@calcom/features/flags/server/router"; import { insightsRouter } from "@calcom/features/insights/server/trpc-router"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; @@ -160,6 +161,17 @@ const publicViewerRouter = router({ } }), slots: slotsRouter, + event: publicProcedure + .input( + z.object({ + username: z.string(), + eventSlug: z.string(), + }) + ) + .query(async ({ ctx, input }) => { + const event = await getPublicEvent(input.username, input.eventSlug, ctx.prisma); + return event; + }), cityTimezones: publicProcedure.query(async () => { /** * Lazy loads third party dependency to avoid loading 1.5Mb for ALL tRPC procedures. diff --git a/packages/ui/components/badge/Badge.tsx b/packages/ui/components/badge/Badge.tsx index cdf2b89adb..f90331dcc9 100644 --- a/packages/ui/components/badge/Badge.tsx +++ b/packages/ui/components/badge/Badge.tsx @@ -1,6 +1,6 @@ import type { VariantProps } from "class-variance-authority"; import { cva } from "class-variance-authority"; -import type { ComponentProps, ReactNode } from "react"; +import React from "react"; import { GoPrimitiveDot } from "react-icons/go"; import classNames from "@calcom/lib/classNames"; @@ -41,19 +41,40 @@ type IconOrDot = } | { startIcon?: unknown; withDot?: boolean }; -export type BadgeProps = InferredBadgeStyles & - ComponentProps<"div"> & { children: ReactNode; rounded?: boolean } & IconOrDot; +export type BadgeBaseProps = InferredBadgeStyles & { + children: React.ReactNode; + rounded?: boolean; +} & IconOrDot; + +export type BadgeProps = + /** + * This union type helps TypeScript understand that there's two options for this component: + * Either it's a div element on which the onClick prop is not allowed, or it's a button element + * on which the onClick prop is required. This is because the onClick prop is used to determine + * whether the component should be a button or a div. + */ + | (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLDivElement>, "onClick"> & { onClick?: never }) + | (BadgeBaseProps & Omit<React.HTMLAttributes<HTMLButtonElement>, "onClick"> & { onClick: () => void }); export const Badge = function Badge(props: BadgeProps) { const { variant, className, size, startIcon, withDot, children, rounded, ...passThroughProps } = props; + const isButton = "onClick" in passThroughProps && passThroughProps.onClick !== undefined; const StartIcon = startIcon ? (startIcon as SVGComponent) : undefined; - return ( - <div - className={classNames(badgeStyles({ variant, size }), rounded && "h-5 w-5 rounded-full p-0", className)} - {...passThroughProps}> + const classes = classNames( + badgeStyles({ variant, size }), + rounded && "h-5 w-5 rounded-full p-0", + className + ); + + const Children = () => ( + <> {withDot ? <GoPrimitiveDot className="h-3 w-3 stroke-[3px]" /> : null} {StartIcon ? <StartIcon className="h-3 w-3 stroke-[3px]" /> : null} - <div>{children}</div> - </div> + {children} + </> ); + + const Wrapper = isButton ? "button" : "div"; + + return React.createElement(Wrapper, { ...passThroughProps, className: classes }, <Children />); }; diff --git a/packages/ui/components/form/index.ts b/packages/ui/components/form/index.ts index cab81972dc..c751166c7d 100644 --- a/packages/ui/components/form/index.ts +++ b/packages/ui/components/form/index.ts @@ -22,7 +22,7 @@ export { Select, SelectField, SelectWithValidation, getReactSelectProps } from " export { TimezoneSelect } from "./timezone-select"; export type { ITimezone, ITimezoneOption } from "./timezone-select"; export { DateRangePickerLazy as DateRangePicker } from "./date-range-picker"; -export { BooleanToggleGroup, BooleanToggleGroupField, ToggleGroup, ToggleGroupItem } from "./toggleGroup"; +export { BooleanToggleGroup, BooleanToggleGroupField, ToggleGroup } from "./toggleGroup"; export { DatePicker } from "./datepicker"; export { FormStep, Steps, Stepper } from "./step"; export { WizardForm } from "./wizard"; diff --git a/packages/ui/components/form/inputs/Input.tsx b/packages/ui/components/form/inputs/Input.tsx index c8d891d14d..0e188cb506 100644 --- a/packages/ui/components/form/inputs/Input.tsx +++ b/packages/ui/components/form/inputs/Input.tsx @@ -12,10 +12,10 @@ import { Eye, EyeOff, X } from "../../icon"; import { HintsOrErrors } from "./HintOrErrors"; import { Label } from "./Label"; -type InputProps = JSX.IntrinsicElements["input"] & { isFullWidth?: boolean }; +type InputProps = JSX.IntrinsicElements["input"] & { isFullWidth?: boolean; isStandaloneField?: boolean }; export const Input = forwardRef<HTMLInputElement, InputProps>(function Input( - { isFullWidth = true, ...props }, + { isFullWidth = true, isStandaloneField = true, ...props }, ref ) { return ( @@ -140,6 +140,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function type={type} placeholder={placeholder} isFullWidth={inputIsFullWidth} + isStandaloneField={false} className={classNames( className, "disabled:bg-muted disabled:hover:border-subtle disabled:cursor-not-allowed", @@ -162,10 +163,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function {addOnSuffix && ( <Addon isFilled={addOnFilled} - className={classNames( - "ltr:rounded-r-md ltr:border-l-0 rtl:rounded-l-md rtl:border-r-0", - addOnClassname - )}> + className={classNames("ltr:rounded-r-md rtl:rounded-l-md", addOnClassname)}> {addOnSuffix} </Addon> )} @@ -231,10 +229,7 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct addOnFilled={false} addOnSuffix={ <Tooltip content={textLabel}> - <button - className="text-emphasis absolute bottom-0 h-9 ltr:right-3 rtl:left-3" - type="button" - onClick={() => toggleIsPasswordVisible()}> + <button className="text-emphasis h-9" type="button" onClick={() => toggleIsPasswordVisible()}> {isPasswordVisible ? ( <EyeOff className="h-4 stroke-[2.5px]" /> ) : ( diff --git a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx index 129260b1e2..5977a7c6bc 100644 --- a/packages/ui/components/form/timezone-select/TimezoneSelect.tsx +++ b/packages/ui/components/form/timezone-select/TimezoneSelect.tsx @@ -15,6 +15,7 @@ export interface ICity { export function TimezoneSelect({ className, + classNames: timezoneClassNames, components, variant = "default", ...props @@ -49,13 +50,15 @@ export function TimezoneSelect({ formatOptionLabel={(option) => <p className="truncate">{(option as ITimezoneOption).value}</p>} getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, cities)} classNames={{ - input: () => classNames("text-emphasis", props.classNames?.input), + ...timezoneClassNames, + input: (state) => + classNames("text-emphasis", timezoneClassNames?.input && timezoneClassNames.input(state)), option: (state) => classNames( "bg-default flex cursor-pointer justify-between py-2.5 px-3 rounded-none text-default ", state.isFocused && "bg-subtle", state.isSelected && "bg-emphasis text-default", - props.classNames?.option + timezoneClassNames?.option && timezoneClassNames.option(state) ), placeholder: (state) => classNames("text-muted", state.isFocused && "hidden"), dropdownIndicator: () => "text-default", @@ -64,33 +67,44 @@ export function TimezoneSelect({ variant === "default" ? "px-3 py-2 bg-default border-default !min-h-9 text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-2 focus-within:ring-emphasis hover:border-emphasis rounded-md border gap-1" : "text-sm gap-1", - props.classNames?.control + timezoneClassNames?.control && timezoneClassNames.control(state) ), - singleValue: () => classNames("text-emphasis placeholder:text-muted", props.classNames?.singleValue), - valueContainer: () => - classNames("text-emphasis placeholder:text-muted flex gap-1", props.classNames?.valueContainer), - multiValue: () => + singleValue: (state) => + classNames( + "text-emphasis placeholder:text-muted", + timezoneClassNames?.singleValue && timezoneClassNames.singleValue(state) + ), + valueContainer: (state) => + classNames( + "text-emphasis placeholder:text-muted flex gap-1", + timezoneClassNames?.valueContainer && timezoneClassNames.valueContainer(state) + ), + multiValue: (state) => classNames( "bg-subtle text-default rounded-md py-1.5 px-2 flex items-center text-sm leading-none", - props.classNames?.multiValue + timezoneClassNames?.multiValue && timezoneClassNames.multiValue(state) ), - menu: () => + menu: (state) => classNames( "rounded-md bg-default text-sm leading-4 text-default mt-1 border border-subtle", - props.classNames?.menu + timezoneClassNames?.menu && timezoneClassNames.menu(state) ), groupHeading: () => "leading-none text-xs uppercase text-default pl-2.5 pt-4 pb-2", - menuList: () => classNames("scroll-bar scrollbar-track-w-20 rounded-md", props.classNames?.menuList), + menuList: (state) => + classNames( + "scroll-bar scrollbar-track-w-20 rounded-md", + timezoneClassNames?.menuList && timezoneClassNames.menuList(state) + ), indicatorsContainer: (state) => classNames( state.selectProps.menuIsOpen ? state.isMulti ? "[&>*:last-child]:rotate-180 [&>*:last-child]:transition-transform" : "rotate-180 transition-transform" - : "text-default" // Woo it adds another SVG here on multi for some reason + : "text-default", // Woo it adds another SVG here on multi for some reason + timezoneClassNames?.indicatorsContainer && timezoneClassNames.indicatorsContainer(state) ), multiValueRemove: () => "text-default py-auto ml-2", - ...props.classNames, }} /> ); diff --git a/packages/ui/components/form/toggleGroup/ToggleGroup.tsx b/packages/ui/components/form/toggleGroup/ToggleGroup.tsx index cc942b987f..dfe1584f82 100644 --- a/packages/ui/components/form/toggleGroup/ToggleGroup.tsx +++ b/packages/ui/components/form/toggleGroup/ToggleGroup.tsx @@ -1,15 +1,32 @@ import * as RadixToggleGroup from "@radix-ui/react-toggle-group"; +import type { ReactNode } from "react"; import { useEffect, useState } from "react"; import { classNames } from "@calcom/lib"; - -export const ToggleGroupItem = () => <div>hi</div>; +import { Tooltip } from "@calcom/ui"; interface ToggleGroupProps extends Omit<RadixToggleGroup.ToggleGroupSingleProps, "type"> { - options: { value: string; label: string; disabled?: boolean }[]; + options: { value: string; label: string | ReactNode; disabled?: boolean; tooltip?: string }[]; isFullWidth?: boolean; } +const OptionalTooltipWrapper = ({ + children, + tooltipText, +}: { + children: ReactNode; + tooltipText?: ReactNode; +}) => { + if (tooltipText) { + return ( + <Tooltip delayDuration={150} sideOffset={12} side="bottom" content={tooltipText}> + {children} + </Tooltip> + ); + } + return <>{children}</>; +}; + export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: ToggleGroupProps) => { const [value, setValue] = useState<string | undefined>(props.defaultValue); const [activeToggleElement, setActiveToggleElement] = useState<null | HTMLButtonElement>(null); @@ -36,25 +53,26 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T style={{ left: activeToggleElement?.offsetLeft, width: activeToggleElement?.offsetWidth }} /> {options.map((option) => ( - <RadixToggleGroup.Item - disabled={option.disabled} - key={option.value} - value={option.value} - className={classNames( - "relative rounded-[4px] px-3 py-1 text-sm leading-tight", - option.disabled - ? " text-gray-400 hover:cursor-not-allowed" - : " text-default [&[aria-checked='false']]:hover:bg-subtle", - isFullWidth && "w-full" - )} - ref={(node) => { - if (node && value === option.value) { - setActiveToggleElement(node); - } - return node; - }}> - {option.label} - </RadixToggleGroup.Item> + <OptionalTooltipWrapper key={option.value} tooltipText={option.tooltip}> + <RadixToggleGroup.Item + disabled={option.disabled} + value={option.value} + className={classNames( + "relative rounded-[4px] px-3 py-1 text-sm leading-tight", + option.disabled + ? "text-gray-400 hover:cursor-not-allowed" + : "text-default [&[aria-checked='false']]:hover:bg-subtle", + isFullWidth && "w-full" + )} + ref={(node) => { + if (node && value === option.value && activeToggleElement !== node) { + setActiveToggleElement(node); + } + return node; + }}> + {option.label} + </RadixToggleGroup.Item> + </OptionalTooltipWrapper> ))} </RadixToggleGroup.Root> </> diff --git a/packages/ui/components/form/toggleGroup/index.ts b/packages/ui/components/form/toggleGroup/index.ts index 582aaebb30..a111fc9c00 100644 --- a/packages/ui/components/form/toggleGroup/index.ts +++ b/packages/ui/components/form/toggleGroup/index.ts @@ -1,2 +1,2 @@ -export { ToggleGroup, ToggleGroupItem } from "./ToggleGroup"; +export { ToggleGroup } from "./ToggleGroup"; export { BooleanToggleGroup, BooleanToggleGroupField } from "./BooleanToggleGroup"; diff --git a/packages/ui/components/tooltip/Tooltip.tsx b/packages/ui/components/tooltip/Tooltip.tsx index 6fa833f68d..80aba46461 100644 --- a/packages/ui/components/tooltip/Tooltip.tsx +++ b/packages/ui/components/tooltip/Tooltip.tsx @@ -1,7 +1,7 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import React from "react"; -import { classNames } from "@calcom/lib"; +import classNames from "@calcom/lib/classNames"; export function Tooltip({ children, @@ -9,19 +9,21 @@ export function Tooltip({ open, defaultOpen, onOpenChange, + delayDuration, side = "top", ...props }: { children: React.ReactNode; content: React.ReactNode; + delayDuration?: number; open?: boolean; defaultOpen?: boolean; side?: "top" | "right" | "bottom" | "left"; onOpenChange?: (open: boolean) => void; -}) { +} & TooltipPrimitive.TooltipContentProps) { return ( <TooltipPrimitive.Root - delayDuration={50} + delayDuration={delayDuration || 50} open={open} defaultOpen={defaultOpen} onOpenChange={onOpenChange}> @@ -31,7 +33,7 @@ export function Tooltip({ className={classNames( side === "top" && "-mt-7", side === "right" && "ml-2", - "bg-inverted text-inverted relative rounded-md px-2 py-1 text-xs font-semibold shadow-lg" + "bg-inverted text-inverted relative relative z-20 rounded-md px-2 py-1 text-xs font-semibold shadow-lg" )} side={side} align="center" diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 707aa31923..5d86e20889 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -33,7 +33,6 @@ export { DateRangePicker, MultiSelectCheckbox, ToggleGroup, - ToggleGroupItem, getReactSelectProps, ColorPicker, FormStep, diff --git a/yarn.lock b/yarn.lock index b671ebaec5..e1ee54d140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -122,25 +122,6 @@ __metadata: languageName: node linkType: hard -"@auth/core@npm:^0.1.4": - version: 0.1.4 - resolution: "@auth/core@npm:0.1.4" - dependencies: - "@panva/hkdf": 1.0.2 - cookie: 0.5.0 - jose: 4.11.1 - oauth4webapi: 2.0.5 - preact: 10.11.3 - preact-render-to-string: 5.2.3 - peerDependencies: - nodemailer: 6.8.0 - peerDependenciesMeta: - nodemailer: - optional: true - checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79 - languageName: node - linkType: hard - "@aws-crypto/ie11-detection@npm:^3.0.0": version: 3.0.0 resolution: "@aws-crypto/ie11-detection@npm:3.0.0" @@ -3083,7 +3064,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.16.7": +"@babel/plugin-transform-react-jsx-source@npm:^7.16.7, @babel/plugin-transform-react-jsx-source@npm:^7.19.6": version: 7.19.6 resolution: "@babel/plugin-transform-react-jsx-source@npm:7.19.6" dependencies: @@ -3120,7 +3101,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx@npm:^7.17.3": +"@babel/plugin-transform-react-jsx@npm:^7.17.3, @babel/plugin-transform-react-jsx@npm:^7.19.0": version: 7.21.0 resolution: "@babel/plugin-transform-react-jsx@npm:7.21.0" dependencies: @@ -3897,6 +3878,8 @@ __metadata: "@calcom/core": "*" "@calcom/dayjs": "*" "@calcom/emails": "*" + "@calcom/embed-core": "*" + "@calcom/embed-snippet": "*" "@calcom/features": "*" "@calcom/lib": "*" "@calcom/prisma": "*" @@ -3909,16 +3892,17 @@ __metadata: jest: ^28.1.0 memory-cache: ^0.2.0 modify-response-middleware: ^1.1.0 - next: ^13.2.1 + next: ^12.3.1 next-api-middleware: ^1.0.1 - next-axiom: ^0.16.0 + next-axiom: ^0.10.0 next-swagger-doc: ^0.3.4 + next-transpile-modules: ^9.0.0 next-validations: ^0.2.0 node-mocks-http: ^1.11.0 - typescript: ^4.9.4 + typescript: ^4.7.4 tzdata: ^1.0.30 uuid: ^8.3.2 - zod: ^3.20.2 + zod: ^3.19.1 languageName: unknown linkType: soft @@ -3980,36 +3964,17 @@ __metadata: languageName: unknown linkType: soft -"@calcom/auth@workspace:apps/auth": +"@calcom/atoms@workspace:packages/atoms": version: 0.0.0-use.local - resolution: "@calcom/auth@workspace:apps/auth" + resolution: "@calcom/atoms@workspace:packages/atoms" dependencies: - "@auth/core": ^0.1.4 - "@calcom/app-store": "*" - "@calcom/app-store-cli": "*" - "@calcom/config": "*" - "@calcom/core": "*" - "@calcom/dayjs": "*" - "@calcom/embed-core": "workspace:*" - "@calcom/embed-react": "workspace:*" - "@calcom/embed-snippet": "workspace:*" - "@calcom/features": "*" - "@calcom/lib": "*" - "@calcom/prisma": "*" - "@calcom/trpc": "*" - "@calcom/tsconfig": "*" - "@calcom/types": "*" - "@calcom/ui": "*" - "@types/node": 16.9.1 - "@types/react": 18.0.26 - "@types/react-dom": 18.0.9 - eslint: ^8.34.0 - eslint-config-next: ^13.2.1 - next: ^13.2.1 - next-auth: ^4.20.1 - react: ^18.2.0 - react-dom: ^18.2.0 - typescript: ^4.9.4 + "@rollup/plugin-node-resolve": ^15.0.1 + "@types/react": ^18.0.25 + "@types/react-dom": ^18.0.9 + "@vitejs/plugin-react": ^2.2.0 + rollup-plugin-node-builtins: ^2.1.2 + typescript: ^4.9.3 + vite: ^3.2.4 languageName: unknown linkType: soft @@ -4050,6 +4015,7 @@ __metadata: resolution: "@calcom/config@workspace:packages/config" dependencies: "@calcom/eslint-plugin-eslint": "*" + "@savvywombat/tailwindcss-grid-areas": ^3.0.0 "@tailwindcss/forms": ^0.5.2 "@tailwindcss/line-clamp": ^0.4.0 "@tailwindcss/typography": ^0.5.4 @@ -4077,34 +4043,37 @@ __metadata: resolution: "@calcom/console@workspace:apps/console" dependencies: "@calcom/dayjs": "*" + "@calcom/embed-react": "*" "@calcom/features": "*" "@calcom/lib": "*" "@calcom/tsconfig": "*" "@calcom/ui": "*" "@headlessui/react": ^1.5.0 "@heroicons/react": ^1.0.6 - "@prisma/client": ^4.13.0 + "@prisma/client": ^4.7.1 "@tailwindcss/forms": ^0.5.2 "@types/node": 16.9.1 - "@types/react": 18.0.26 + "@types/react": ^18.0.17 autoprefixer: ^10.4.12 chart.js: ^3.7.1 client-only: ^0.0.1 - eslint: ^8.34.0 - next: ^13.2.1 - next-auth: ^4.20.1 + eslint: ^8.22.0 + next: ^12.3.1 + next-auth: ^4.10.3 next-i18next: ^11.3.0 + next-transpile-modules: ^9.0.0 postcss: ^8.4.18 - prisma: ^4.13.0 + prisma: ^4.7.1 prisma-field-encryption: ^1.4.0 react: ^18.2.0 react-chartjs-2: ^4.0.1 react-dom: ^18.2.0 - react-hook-form: ^7.43.3 + react-hook-form: ^7.34.2 react-live-chat-loader: ^2.7.3 swr: ^1.2.2 tailwindcss: ^3.2.1 - typescript: ^4.9.4 + turbo: ^1.4.3 + typescript: ^4.7.4 zod: ^3.20.2 languageName: unknown linkType: soft @@ -4207,7 +4176,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/embed-core@workspace:*, @calcom/embed-core@workspace:packages/embeds/embed-core": +"@calcom/embed-core@*, @calcom/embed-core@workspace:*, @calcom/embed-core@workspace:packages/embeds/embed-core": version: 0.0.0-use.local resolution: "@calcom/embed-core@workspace:packages/embeds/embed-core" dependencies: @@ -4221,7 +4190,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/embed-react@workspace:*, @calcom/embed-react@workspace:^, @calcom/embed-react@workspace:packages/embeds/embed-react": +"@calcom/embed-react@*, @calcom/embed-react@workspace:*, @calcom/embed-react@workspace:^, @calcom/embed-react@workspace:packages/embeds/embed-react": version: 0.0.0-use.local resolution: "@calcom/embed-react@workspace:packages/embeds/embed-react" dependencies: @@ -4241,7 +4210,7 @@ __metadata: languageName: unknown linkType: soft -"@calcom/embed-snippet@workspace:*, @calcom/embed-snippet@workspace:packages/embeds/embed-snippet": +"@calcom/embed-snippet@*, @calcom/embed-snippet@workspace:*, @calcom/embed-snippet@workspace:packages/embeds/embed-snippet": version: 0.0.0-use.local resolution: "@calcom/embed-snippet@workspace:packages/embeds/embed-snippet" dependencies: @@ -4349,9 +4318,13 @@ __metadata: "@calcom/trpc": "*" "@calcom/ui": "*" "@lexical/react": ^0.5.0 + "@testing-library/react-hooks": ^8.0.1 dompurify: ^2.4.1 + framer-motion: ^10.12.3 lexical: ^0.5.0 - zustand: ^4.1.4 + mockdate: ^3.0.5 + react-sticky-box: ^2.0.4 + zustand: ^4.3.2 languageName: unknown linkType: soft @@ -5370,6 +5343,22 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^0.8.2": + version: 0.8.8 + resolution: "@emotion/is-prop-valid@npm:0.8.8" + dependencies: + "@emotion/memoize": 0.7.4 + checksum: bb7ec6d48c572c540e24e47cc94fc2f8dec2d6a342ae97bc9c8b6388d9b8d283862672172a1bb62d335c02662afe6291e10c71e9b8642664a8b43416cdceffac + languageName: node + linkType: hard + +"@emotion/memoize@npm:0.7.4": + version: 0.7.4 + resolution: "@emotion/memoize@npm:0.7.4" + checksum: 4e3920d4ec95995657a37beb43d3f4b7d89fed6caa2b173a4c04d10482d089d5c3ea50bbc96618d918b020f26ed6e9c4026bbd45433566576c1f7b056c3271dc + languageName: node + linkType: hard + "@emotion/memoize@npm:^0.7.4, @emotion/memoize@npm:^0.7.5": version: 0.7.5 resolution: "@emotion/memoize@npm:0.7.5" @@ -5461,6 +5450,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.15.18": + version: 0.15.18 + resolution: "@esbuild/android-arm@npm:0.15.18" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/android-arm@npm:0.16.17" @@ -5531,6 +5527,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.15.18": + version: 0.15.18 + resolution: "@esbuild/linux-loong64@npm:0.15.18" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.16.17": version: 0.16.17 resolution: "@esbuild/linux-loong64@npm:0.16.17" @@ -5615,6 +5618,24 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.2.0": + version: 4.4.0 + resolution: "@eslint-community/eslint-utils@npm:4.4.0" + dependencies: + eslint-visitor-keys: ^3.3.0 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.4.0": + version: 4.5.0 + resolution: "@eslint-community/regexpp@npm:4.5.0" + checksum: 99c01335947dbd7f2129e954413067e217ccaa4e219fe0917b7d2bd96135789384b8fedbfb8eb09584d5130b27a7b876a7150ab7376f51b3a0c377d5ce026a10 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^1.0.5": version: 1.3.0 resolution: "@eslint/eslintrc@npm:1.3.0" @@ -5649,6 +5670,30 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^2.0.2": + version: 2.0.2 + resolution: "@eslint/eslintrc@npm:2.0.2" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.5.1 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: cfcf5e12c7b2c4476482e7f12434e76eae16fcd163ee627309adb10b761e5caa4a4e52ed7be464423320ff3d11eca5b50de5bf8be3e25834222470835dd5c801 + languageName: node + linkType: hard + +"@eslint/js@npm:8.38.0": + version: 8.38.0 + resolution: "@eslint/js@npm:8.38.0" + checksum: 1f28987aa8c9cd93e23384e16c7220863b39b5dc4b66e46d7cdbccce868040f455a98d24cd8b567a884f26545a0555b761f7328d4a00c051e7ef689cbea5fce1 + languageName: node + linkType: hard + "@ethereumjs/common@npm:^2.5.0, @ethereumjs/common@npm:^2.6.3": version: 2.6.3 resolution: "@ethereumjs/common@npm:2.6.3" @@ -7949,6 +7994,13 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:12.3.4": + version: 12.3.4 + resolution: "@next/env@npm:12.3.4" + checksum: daa3fc11efd1344c503eab41311a0e503ba7fd08607eeb3dc571036a6211eb37959cc4ed48b71dcc411cc214e7623ffd02411080aad3e09dc6a1192d5b256e60 + languageName: node + linkType: hard + "@next/env@npm:13.2.3": version: 13.2.3 resolution: "@next/env@npm:13.2.3" @@ -7972,6 +8024,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-android-arm-eabi@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-android-arm-eabi@npm:12.3.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@next/swc-android-arm-eabi@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-android-arm-eabi@npm:13.2.3" @@ -7986,6 +8045,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-android-arm64@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-android-arm64@npm:12.3.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-android-arm64@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-android-arm64@npm:13.2.3" @@ -8000,6 +8066,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-arm64@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-darwin-arm64@npm:12.3.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-darwin-arm64@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-darwin-arm64@npm:13.2.3" @@ -8014,6 +8087,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-x64@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-darwin-x64@npm:12.3.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@next/swc-darwin-x64@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-darwin-x64@npm:13.2.3" @@ -8028,6 +8108,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-freebsd-x64@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-freebsd-x64@npm:12.3.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@next/swc-freebsd-x64@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-freebsd-x64@npm:13.2.3" @@ -8042,6 +8129,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm-gnueabihf@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-linux-arm-gnueabihf@npm:12.3.4" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@next/swc-linux-arm-gnueabihf@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.3" @@ -8056,6 +8150,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-gnu@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-linux-arm64-gnu@npm:12.3.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-arm64-gnu@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-linux-arm64-gnu@npm:13.2.3" @@ -8070,6 +8171,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-musl@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-linux-arm64-musl@npm:12.3.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-arm64-musl@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-linux-arm64-musl@npm:13.2.3" @@ -8084,6 +8192,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-gnu@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-linux-x64-gnu@npm:12.3.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-x64-gnu@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-linux-x64-gnu@npm:13.2.3" @@ -8098,6 +8213,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-musl@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-linux-x64-musl@npm:12.3.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-x64-musl@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-linux-x64-musl@npm:13.2.3" @@ -8112,6 +8234,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-arm64-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-win32-arm64-msvc@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-win32-arm64-msvc@npm:13.2.3" @@ -8126,6 +8255,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-ia32-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@next/swc-win32-ia32-msvc@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-win32-ia32-msvc@npm:13.2.3" @@ -8140,6 +8276,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-x64-msvc@npm:12.3.4": + version: 12.3.4 + resolution: "@next/swc-win32-x64-msvc@npm:12.3.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@next/swc-win32-x64-msvc@npm:13.2.3": version: 13.2.3 resolution: "@next/swc-win32-x64-msvc@npm:13.2.3" @@ -8299,13 +8442,6 @@ __metadata: languageName: node linkType: hard -"@panva/hkdf@npm:1.0.2": - version: 1.0.2 - resolution: "@panva/hkdf@npm:1.0.2" - checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03 - languageName: node - linkType: hard - "@panva/hkdf@npm:^1.0.2": version: 1.0.4 resolution: "@panva/hkdf@npm:1.0.4" @@ -8410,6 +8546,20 @@ __metadata: languageName: node linkType: hard +"@prisma/client@npm:^4.7.1": + version: 4.12.0 + resolution: "@prisma/client@npm:4.12.0" + dependencies: + "@prisma/engines-version": 4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7 + peerDependencies: + prisma: "*" + peerDependenciesMeta: + prisma: + optional: true + checksum: bbd17500ee218a71e765a75b649c56bc0da1903e63d69d716b6a0e6995c8e1cc5265423ba1518a789c3e71b91d93e7937180db2d107e426bd4ad2f51998240f0 + languageName: node + linkType: hard + "@prisma/debug@npm:3.8.1": version: 3.8.1 resolution: "@prisma/debug@npm:3.8.1" @@ -8432,6 +8582,13 @@ __metadata: languageName: node linkType: hard +"@prisma/engines-version@npm:4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7": + version: 4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7 + resolution: "@prisma/engines-version@npm:4.12.0-67.659ef412370fa3b41cd7bf6e94587c1dfb7f67e7" + checksum: 54615d6982db9c50eed6132ad7ab3f32ef93f64d36b7f932b0d6109cd54028ea459293833e48b849a5dd968d934bb3cfb275b9a9172934032f95355d6f663dcf + languageName: node + linkType: hard + "@prisma/engines-version@npm:4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a": version: 4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a resolution: "@prisma/engines-version@npm:4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" @@ -8439,6 +8596,13 @@ __metadata: languageName: node linkType: hard +"@prisma/engines@npm:4.12.0": + version: 4.12.0 + resolution: "@prisma/engines@npm:4.12.0" + checksum: 5d226a2c86bee5bc6fa34910ec9ba9a68a1c0248977d0da47964372edcfe773ee6c4bb00e244258f535570f018d883f0ab9e2677cf06471869bb8857f79880f6 + languageName: node + linkType: hard + "@prisma/engines@npm:4.13.0": version: 4.13.0 resolution: "@prisma/engines@npm:4.13.0" @@ -9816,6 +9980,25 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-node-resolve@npm:^15.0.1": + version: 15.0.1 + resolution: "@rollup/plugin-node-resolve@npm:15.0.1" + dependencies: + "@rollup/pluginutils": ^5.0.1 + "@types/resolve": 1.20.2 + deepmerge: ^4.2.2 + is-builtin-module: ^3.2.0 + is-module: ^1.0.0 + resolve: ^1.22.1 + peerDependencies: + rollup: ^2.78.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 90e30b41626a15ebf02746a83d34b15f9fe9051ddc156a9bf785504f489947980b3bdeb7bf2f80828a9becfe472a03a96d0238328a3e3e2198a482fcac7eb3aa + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^3.1.0": version: 3.1.0 resolution: "@rollup/pluginutils@npm:3.1.0" @@ -9862,6 +10045,17 @@ __metadata: languageName: node linkType: hard +"@savvywombat/tailwindcss-grid-areas@npm:^3.0.0": + version: 3.0.1 + resolution: "@savvywombat/tailwindcss-grid-areas@npm:3.0.1" + dependencies: + lodash: ^4.17.21 + peerDependencies: + tailwindcss: ^3.0.1 + checksum: 016672a0585b2b1bdd0ae2f8ed821d634d2a5ba7ea26484adbc8cd38499366be80c6e338cd93d8c25cf6e29a8f33293eb7e24ff68184bea3b36ea27b9884dbb1 + languageName: node + linkType: hard + "@sendgrid/client@npm:^7.7.0": version: 7.7.0 resolution: "@sendgrid/client@npm:7.7.0" @@ -11867,6 +12061,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:0.4.11": + version: 0.4.11 + resolution: "@swc/helpers@npm:0.4.11" + dependencies: + tslib: ^2.4.0 + checksum: 736857d524b41a8a4db81094e9b027f554004e0fa3e86325d85bdb38f7e6459ce022db079edb6c61ba0f46fe8583b3e663e95f7acbd13e51b8da6c34e45bba2e + languageName: node + linkType: hard + "@swc/helpers@npm:0.4.14": version: 0.4.14 resolution: "@swc/helpers@npm:0.4.14" @@ -12024,6 +12227,28 @@ __metadata: languageName: node linkType: hard +"@testing-library/react-hooks@npm:^8.0.1": + version: 8.0.1 + resolution: "@testing-library/react-hooks@npm:8.0.1" + dependencies: + "@babel/runtime": ^7.12.5 + react-error-boundary: ^3.1.0 + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + checksum: 7fe44352e920deb5cb1876f80d64e48615232072c9d5382f1e0284b3aab46bb1c659a040b774c45cdf084a5257b8fe463f7e08695ad8480d8a15635d4d3d1f6d + languageName: node + linkType: hard + "@testing-library/react@npm:^13.3.0": version: 13.3.0 resolution: "@testing-library/react@npm:13.3.0" @@ -13062,6 +13287,13 @@ __metadata: languageName: node linkType: hard +"@types/resolve@npm:1.20.2": + version: 1.20.2 + resolution: "@types/resolve@npm:1.20.2" + checksum: 61c2cad2499ffc8eab36e3b773945d337d848d3ac6b7b0a87c805ba814bc838ef2f262fc0f109bfd8d2e0898ff8bd80ad1025f9ff64f1f71d3d4294c9f14e5f6 + languageName: node + linkType: hard + "@types/responselike@npm:*, @types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -13645,6 +13877,23 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^2.2.0": + version: 2.2.0 + resolution: "@vitejs/plugin-react@npm:2.2.0" + dependencies: + "@babel/core": ^7.19.6 + "@babel/plugin-transform-react-jsx": ^7.19.0 + "@babel/plugin-transform-react-jsx-development": ^7.18.6 + "@babel/plugin-transform-react-jsx-self": ^7.18.6 + "@babel/plugin-transform-react-jsx-source": ^7.19.6 + magic-string: ^0.26.7 + react-refresh: ^0.14.0 + peerDependencies: + vite: ^3.0.0 + checksum: cc85ab31b4689ab137c4b1e65383dccce494371523eb164c579096e513a2abbaa7efb49ba08655fae9f6692f5b7b2602ad339bdce4ae5982fc08fe444fb8a4e5 + languageName: node + linkType: hard + "@wagmi/core@npm:^0.5.4": version: 0.5.4 resolution: "@wagmi/core@npm:0.5.4" @@ -14349,6 +14598,15 @@ __metadata: languageName: node linkType: hard +"abstract-leveldown@npm:~0.12.0, abstract-leveldown@npm:~0.12.1": + version: 0.12.4 + resolution: "abstract-leveldown@npm:0.12.4" + dependencies: + xtend: ~3.0.0 + checksum: e300f04bb638cc9c462f6e8fa925672e51beb24c1470c39ece709e54f2f499661ac5fe0119175c7dcb6e32c843423d6960009d4d24e72526478b261163e8070b + languageName: node + linkType: hard + "accept-language-parser@npm:^1.5.0": version: 1.5.0 resolution: "accept-language-parser@npm:1.5.0" @@ -15919,6 +16177,15 @@ __metadata: languageName: node linkType: hard +"bl@npm:~0.8.1": + version: 0.8.2 + resolution: "bl@npm:0.8.2" + dependencies: + readable-stream: ~1.0.26 + checksum: 18767c5c861ae1cdbb000bb346e9e8e29137225e8eef97f39db78beeb236beca609f465580c5c1b177d621505f57400834fb4a17a66d264f33a0237293ec2ac5 + languageName: node + linkType: hard + "blakejs@npm:^1.1.0": version: 1.2.1 resolution: "blakejs@npm:1.2.1" @@ -16183,6 +16450,17 @@ __metadata: languageName: node linkType: hard +"browserify-fs@npm:^1.0.0": + version: 1.0.0 + resolution: "browserify-fs@npm:1.0.0" + dependencies: + level-filesystem: ^1.0.1 + level-js: ^2.1.3 + levelup: ^0.18.2 + checksum: e0c35cf42c839c0a217048b1671d91ee6e53fd05f163db4f809e46c2f6264f784768e7c850abc200b0eaca378d42e00e01876eda21fd84fc0a4280bd6200a9c3 + languageName: node + linkType: hard + "browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.0.1": version: 4.1.0 resolution: "browserify-rsa@npm:4.1.0" @@ -16383,6 +16661,13 @@ __metadata: languageName: node linkType: hard +"buffer-es6@npm:^4.9.2": + version: 4.9.3 + resolution: "buffer-es6@npm:4.9.3" + checksum: dfc8ebb3c5c00166e6f81e6ec7ea876693ea6197a8d0b07b1a17482ffab0e5d3307bfb539f84862b1ae35cd70ad03835db0f3c7dc4e337cbd16c50bb4c7e5df7 + languageName: node + linkType: hard + "buffer-fill@npm:^1.0.0": version: 1.0.0 resolution: "buffer-fill@npm:1.0.0" @@ -16469,6 +16754,13 @@ __metadata: languageName: node linkType: hard +"builtin-modules@npm:^3.3.0": + version: 3.3.0 + resolution: "builtin-modules@npm:3.3.0" + checksum: db021755d7ed8be048f25668fe2117620861ef6703ea2c65ed2779c9e3636d5c3b82325bd912244293959ff3ae303afa3471f6a15bf5060c103e4cc3a839749d + languageName: node + linkType: hard + "builtin-status-codes@npm:^3.0.0": version: 3.0.0 resolution: "builtin-status-codes@npm:3.0.0" @@ -17398,6 +17690,13 @@ __metadata: languageName: node linkType: hard +"clone@npm:~0.1.9": + version: 0.1.19 + resolution: "clone@npm:0.1.19" + checksum: 5e710e16da67abe30c0664c8fd69c280635be59a4fae0a5fe58ed324e701e99348b48ce67288716fa223edd42ba574e58a3783cb2fcfa381b8b49ce7e56ac3f4 + languageName: node + linkType: hard + "clsx@npm:1.1.0": version: 1.1.0 resolution: "clsx@npm:1.1.0" @@ -17727,7 +18026,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.5.0": +"concat-stream@npm:^1.4.4, concat-stream@npm:^1.5.0": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -18838,6 +19137,15 @@ __metadata: languageName: node linkType: hard +"deferred-leveldown@npm:~0.2.0": + version: 0.2.0 + resolution: "deferred-leveldown@npm:0.2.0" + dependencies: + abstract-leveldown: ~0.12.1 + checksum: f7690ec5b1e951e6f56998be26dd0a1331ef28cb7eaa9e090a282780d47dc006effd4b82a2a82b636cae801378047997aca10c0b44b09c8624633cdb96b07913 + languageName: node + linkType: hard + "define-lazy-prop@npm:^2.0.0": version: 2.0.0 resolution: "define-lazy-prop@npm:2.0.0" @@ -19728,7 +20036,7 @@ __metadata: languageName: node linkType: hard -"errno@npm:^0.1.3, errno@npm:~0.1.7": +"errno@npm:^0.1.1, errno@npm:^0.1.3, errno@npm:~0.1.1, errno@npm:~0.1.7": version: 0.1.8 resolution: "errno@npm:0.1.8" dependencies: @@ -19982,6 +20290,13 @@ __metadata: languageName: node linkType: hard +"esbuild-android-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-android-64@npm:0.15.18" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "esbuild-android-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-android-arm64@npm:0.14.54" @@ -19989,6 +20304,13 @@ __metadata: languageName: node linkType: hard +"esbuild-android-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-android-arm64@npm:0.15.18" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "esbuild-darwin-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-darwin-64@npm:0.14.54" @@ -19996,6 +20318,13 @@ __metadata: languageName: node linkType: hard +"esbuild-darwin-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-darwin-64@npm:0.15.18" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "esbuild-darwin-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-darwin-arm64@npm:0.14.54" @@ -20003,6 +20332,13 @@ __metadata: languageName: node linkType: hard +"esbuild-darwin-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-darwin-arm64@npm:0.15.18" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "esbuild-freebsd-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-freebsd-64@npm:0.14.54" @@ -20010,6 +20346,13 @@ __metadata: languageName: node linkType: hard +"esbuild-freebsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-freebsd-64@npm:0.15.18" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "esbuild-freebsd-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-freebsd-arm64@npm:0.14.54" @@ -20017,6 +20360,13 @@ __metadata: languageName: node linkType: hard +"esbuild-freebsd-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-freebsd-arm64@npm:0.15.18" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "esbuild-linux-32@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-32@npm:0.14.54" @@ -20024,6 +20374,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-32@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-32@npm:0.15.18" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "esbuild-linux-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-64@npm:0.14.54" @@ -20031,6 +20388,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-64@npm:0.15.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "esbuild-linux-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-arm64@npm:0.14.54" @@ -20038,6 +20402,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-arm64@npm:0.15.18" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "esbuild-linux-arm@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-arm@npm:0.14.54" @@ -20045,6 +20416,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-arm@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-arm@npm:0.15.18" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "esbuild-linux-mips64le@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-mips64le@npm:0.14.54" @@ -20052,6 +20430,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-mips64le@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-mips64le@npm:0.15.18" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "esbuild-linux-ppc64le@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-ppc64le@npm:0.14.54" @@ -20059,6 +20444,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-ppc64le@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-ppc64le@npm:0.15.18" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "esbuild-linux-riscv64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-riscv64@npm:0.14.54" @@ -20066,6 +20458,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-riscv64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-riscv64@npm:0.15.18" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "esbuild-linux-s390x@npm:0.14.54": version: 0.14.54 resolution: "esbuild-linux-s390x@npm:0.14.54" @@ -20073,6 +20472,13 @@ __metadata: languageName: node linkType: hard +"esbuild-linux-s390x@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-linux-s390x@npm:0.15.18" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "esbuild-netbsd-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-netbsd-64@npm:0.14.54" @@ -20080,6 +20486,13 @@ __metadata: languageName: node linkType: hard +"esbuild-netbsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-netbsd-64@npm:0.15.18" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "esbuild-openbsd-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-openbsd-64@npm:0.14.54" @@ -20087,6 +20500,13 @@ __metadata: languageName: node linkType: hard +"esbuild-openbsd-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-openbsd-64@npm:0.15.18" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "esbuild-sunos-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-sunos-64@npm:0.14.54" @@ -20094,6 +20514,13 @@ __metadata: languageName: node linkType: hard +"esbuild-sunos-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-sunos-64@npm:0.15.18" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "esbuild-windows-32@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-32@npm:0.14.54" @@ -20101,6 +20528,13 @@ __metadata: languageName: node linkType: hard +"esbuild-windows-32@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-32@npm:0.15.18" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "esbuild-windows-64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-64@npm:0.14.54" @@ -20108,6 +20542,13 @@ __metadata: languageName: node linkType: hard +"esbuild-windows-64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-64@npm:0.15.18" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "esbuild-windows-arm64@npm:0.14.54": version: 0.14.54 resolution: "esbuild-windows-arm64@npm:0.14.54" @@ -20115,6 +20556,13 @@ __metadata: languageName: node linkType: hard +"esbuild-windows-arm64@npm:0.15.18": + version: 0.15.18 + resolution: "esbuild-windows-arm64@npm:0.15.18" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "esbuild@npm:^0.14.27": version: 0.14.54 resolution: "esbuild@npm:0.14.54" @@ -20189,6 +20637,83 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.15.9": + version: 0.15.18 + resolution: "esbuild@npm:0.15.18" + dependencies: + "@esbuild/android-arm": 0.15.18 + "@esbuild/linux-loong64": 0.15.18 + esbuild-android-64: 0.15.18 + esbuild-android-arm64: 0.15.18 + esbuild-darwin-64: 0.15.18 + esbuild-darwin-arm64: 0.15.18 + esbuild-freebsd-64: 0.15.18 + esbuild-freebsd-arm64: 0.15.18 + esbuild-linux-32: 0.15.18 + esbuild-linux-64: 0.15.18 + esbuild-linux-arm: 0.15.18 + esbuild-linux-arm64: 0.15.18 + esbuild-linux-mips64le: 0.15.18 + esbuild-linux-ppc64le: 0.15.18 + esbuild-linux-riscv64: 0.15.18 + esbuild-linux-s390x: 0.15.18 + esbuild-netbsd-64: 0.15.18 + esbuild-openbsd-64: 0.15.18 + esbuild-sunos-64: 0.15.18 + esbuild-windows-32: 0.15.18 + esbuild-windows-64: 0.15.18 + esbuild-windows-arm64: 0.15.18 + dependenciesMeta: + "@esbuild/android-arm": + optional: true + "@esbuild/linux-loong64": + optional: true + esbuild-android-64: + optional: true + esbuild-android-arm64: + optional: true + esbuild-darwin-64: + optional: true + esbuild-darwin-arm64: + optional: true + esbuild-freebsd-64: + optional: true + esbuild-freebsd-arm64: + optional: true + esbuild-linux-32: + optional: true + esbuild-linux-64: + optional: true + esbuild-linux-arm: + optional: true + esbuild-linux-arm64: + optional: true + esbuild-linux-mips64le: + optional: true + esbuild-linux-ppc64le: + optional: true + esbuild-linux-riscv64: + optional: true + esbuild-linux-s390x: + optional: true + esbuild-netbsd-64: + optional: true + esbuild-openbsd-64: + optional: true + esbuild-sunos-64: + optional: true + esbuild-windows-32: + optional: true + esbuild-windows-64: + optional: true + esbuild-windows-arm64: + optional: true + bin: + esbuild: bin/esbuild + checksum: ec12682b2cb2d4f0669d0e555028b87a9284ca7f6a1b26e35e69a8697165b35cc682ad598abc70f0bbcfdc12ca84ef888caf5ceee389237862e8f8c17da85f89 + languageName: node + linkType: hard + "esbuild@npm:^0.16.14": version: 0.16.17 resolution: "esbuild@npm:0.16.17" @@ -20638,6 +21163,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^3.4.0": + version: 3.4.0 + resolution: "eslint-visitor-keys@npm:3.4.0" + checksum: 33159169462d3989321a1ec1e9aaaf6a24cc403d5d347e9886d1b5bfe18ffa1be73bdc6203143a28a606b142b1af49787f33cff0d6d0813eb5f2e8d2e1a6043c + languageName: node + linkType: hard + "eslint@npm:8.4.1": version: 8.4.1 resolution: "eslint@npm:8.4.1" @@ -20686,6 +21218,56 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.22.0": + version: 8.38.0 + resolution: "eslint@npm:8.38.0" + dependencies: + "@eslint-community/eslint-utils": ^4.2.0 + "@eslint-community/regexpp": ^4.4.0 + "@eslint/eslintrc": ^2.0.2 + "@eslint/js": 8.38.0 + "@humanwhocodes/config-array": ^0.11.8 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.1.1 + eslint-visitor-keys: ^3.4.0 + espree: ^9.5.1 + esquery: ^1.4.2 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-sdsl: ^4.1.4 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: 73b6d9b650d0434aa7c07d0a1802f099b086ee70a8d8ba7be730439a26572a5eb71def12125c82942be2ec8ee5be38a6f1b42a13e40d4b67f11a148ec9e263eb + languageName: node + linkType: hard + "eslint@npm:^8.34.0": version: 8.34.0 resolution: "eslint@npm:8.34.0" @@ -20779,6 +21361,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^9.5.1": + version: 9.5.1 + resolution: "espree@npm:9.5.1" + dependencies: + acorn: ^8.8.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^3.4.0 + checksum: cdf6e43540433d917c4f2ee087c6e987b2063baa85a1d9cdaf51533d78275ebd5910c42154e7baf8e3e89804b386da0a2f7fad2264d8f04420e7506bf87b3b88 + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -20798,6 +21391,15 @@ __metadata: languageName: node linkType: hard +"esquery@npm:^1.4.2": + version: 1.5.0 + resolution: "esquery@npm:1.5.0" + dependencies: + estraverse: ^5.1.0 + checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 + languageName: node + linkType: hard + "esrecurse@npm:^4.1.0, esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -22105,6 +22707,13 @@ __metadata: languageName: node linkType: hard +"foreach@npm:~2.0.1": + version: 2.0.6 + resolution: "foreach@npm:2.0.6" + checksum: f7b68494545ee41cbd0b0425ebf5386c265dc38ef2a9b0d5cd91a1b82172e939b4cf9387f8e0ebf6db4e368fc79ed323f2198424d5c774515ac3ed9b08901c0e + languageName: node + linkType: hard + "foreground-child@npm:^2.0.0": version: 2.0.0 resolution: "foreground-child@npm:2.0.0" @@ -22284,6 +22893,27 @@ __metadata: languageName: node linkType: hard +"framer-motion@npm:^10.12.3": + version: 10.12.3 + resolution: "framer-motion@npm:10.12.3" + dependencies: + "@emotion/is-prop-valid": ^0.8.2 + tslib: ^2.4.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependenciesMeta: + "@emotion/is-prop-valid": + optional: true + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: c292da47b5bcb313e3db2ffe19e61b3c76bf59f4a45dc72f62a3d9b33f58533d420aced47d5e9eb06be20be97651c937ae91aebb93d8bad6d0412c2768715956 + languageName: node + linkType: hard + "fresh@npm:0.5.2, fresh@npm:^0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -22503,6 +23133,15 @@ __metadata: languageName: node linkType: hard +"fwd-stream@npm:^1.0.4": + version: 1.0.4 + resolution: "fwd-stream@npm:1.0.4" + dependencies: + readable-stream: ~1.0.26-4 + checksum: db4dcf68f214b3fabd6cd9658630dfd1d7ed8d43f7f45408027a90220cd75276e782d1e958821775d7a3a4a83034778e75a097bdc7002c758e8896f76213c65d + languageName: node + linkType: hard + "gauge@npm:^3.0.0": version: 3.0.2 resolution: "gauge@npm:3.0.2" @@ -24111,6 +24750,13 @@ __metadata: languageName: node linkType: hard +"idb-wrapper@npm:^1.5.0": + version: 1.7.2 + resolution: "idb-wrapper@npm:1.7.2" + checksum: a5fa3a771166205e2d5d2b93c66bd31571dada3526b59bc0f8583efb091b6b327125f1a964a25a281b85ef1c44af10a3c511652632ad3adf8229a161132d66ae + languageName: node + linkType: hard + "idna-uts46-hx@npm:^2.3.1": version: 2.3.1 resolution: "idna-uts46-hx@npm:2.3.1" @@ -24255,6 +24901,13 @@ __metadata: languageName: node linkType: hard +"indexof@npm:~0.0.1": + version: 0.0.1 + resolution: "indexof@npm:0.0.1" + checksum: 0fb04e8b147b8585d981a6df1564f25bb3678d6fa74e33e5cecc1464b10f78e15e8ef6bb688f135fe5c2844a128fac8a7831cbe5adc81fdcf12681b093dfcc25 + languageName: node + linkType: hard + "infer-owner@npm:^1.0.3, infer-owner@npm:^1.0.4": version: 1.0.4 resolution: "infer-owner@npm:1.0.4" @@ -24622,6 +25275,15 @@ __metadata: languageName: node linkType: hard +"is-builtin-module@npm:^3.2.0": + version: 3.2.1 + resolution: "is-builtin-module@npm:3.2.1" + dependencies: + builtin-modules: ^3.3.0 + checksum: e8f0ffc19a98240bda9c7ada84d846486365af88d14616e737d280d378695c8c448a621dcafc8332dbf0fcd0a17b0763b845400709963fa9151ddffece90ae88 + languageName: node + linkType: hard + "is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" @@ -24899,6 +25561,13 @@ __metadata: languageName: node linkType: hard +"is-module@npm:^1.0.0": + version: 1.0.0 + resolution: "is-module@npm:1.0.0" + checksum: 8cd5390730c7976fb4e8546dd0b38865ee6f7bacfa08dfbb2cc07219606755f0b01709d9361e01f13009bbbd8099fa2927a8ed665118a6105d66e40f1b838c3f + languageName: node + linkType: hard + "is-nan@npm:^1.3.2": version: 1.3.2 resolution: "is-nan@npm:1.3.2" @@ -24969,6 +25638,13 @@ __metadata: languageName: node linkType: hard +"is-object@npm:~0.1.2": + version: 0.1.2 + resolution: "is-object@npm:0.1.2" + checksum: 7e500b15f4748278ea0a8d43b1283e75e866c055e4a790389087ce652eab8a9343fd74710738f0fdf13a323c31330d65bdcc106f38e9bb7bc0b9c60ae3fd2a2d + languageName: node + linkType: hard + "is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -25227,6 +25903,20 @@ __metadata: languageName: node linkType: hard +"is@npm:~0.2.6": + version: 0.2.7 + resolution: "is@npm:0.2.7" + checksum: 45cea1e6deb41150b5753e18041a833657313e9c791c73f96fb9014b613346f5af2e6650858ef50ea6262c79555b65e09b13d30a268139863885025dd65f1059 + languageName: node + linkType: hard + +"isarray@npm:0.0.1": + version: 0.0.1 + resolution: "isarray@npm:0.0.1" + checksum: 49191f1425681df4a18c2f0f93db3adb85573bcdd6a4482539d98eac9e705d8961317b01175627e860516a2fc45f8f9302db26e5a380a97a520e272e2a40a8d4 + languageName: node + linkType: hard + "isarray@npm:1.0.0, isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -25241,6 +25931,13 @@ __metadata: languageName: node linkType: hard +"isbuffer@npm:~0.0.0": + version: 0.0.0 + resolution: "isbuffer@npm:0.0.0" + checksum: 9796296d3c493974c1f71ccf3170cc8007217a19ce8b3b9dedffd32e8ccc3ac42473b572bbf1b24b86143e826ea157aead11fd1285389518abab76c7da5f50ed + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -26047,13 +26744,6 @@ __metadata: languageName: node linkType: hard -"jose@npm:4.11.1": - version: 4.11.1 - resolution: "jose@npm:4.11.1" - checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7 - languageName: node - linkType: hard - "jose@npm:4.12.0": version: 4.12.0 resolution: "jose@npm:4.12.0" @@ -26741,6 +27431,109 @@ __metadata: languageName: node linkType: hard +"level-blobs@npm:^0.1.7": + version: 0.1.7 + resolution: "level-blobs@npm:0.1.7" + dependencies: + level-peek: 1.0.6 + once: ^1.3.0 + readable-stream: ^1.0.26-4 + checksum: e3cf78ef0bc64ff350edb4e247b2689cd4f5facf1119694ca8c96c28a05a38dc9d88e0bd065b18af65330bc22f5d588719a5c3e63adaa5feba5ea7913f87bebe + languageName: node + linkType: hard + +"level-filesystem@npm:^1.0.1": + version: 1.2.0 + resolution: "level-filesystem@npm:1.2.0" + dependencies: + concat-stream: ^1.4.4 + errno: ^0.1.1 + fwd-stream: ^1.0.4 + level-blobs: ^0.1.7 + level-peek: ^1.0.6 + level-sublevel: ^5.2.0 + octal: ^1.0.0 + once: ^1.3.0 + xtend: ^2.2.0 + checksum: a29e6a9d8c1879d43610113d1bcb59368685ec0ae413fcf0f8dcbb0a0c26b88fcf16f7481acb2b4650e5951ba0635e73a2c8fbe25cd599c50f80949a5547a367 + languageName: node + linkType: hard + +"level-fix-range@npm:2.0": + version: 2.0.0 + resolution: "level-fix-range@npm:2.0.0" + dependencies: + clone: ~0.1.9 + checksum: 250cefa69e1035d1412b4ba3e5cab83cceb894aa833fb0a93417d8d6230c60f6f8154feffbd0f116461ddd441b909e7df1323355d3e1769b3bb20a55729145b5 + languageName: node + linkType: hard + +"level-fix-range@npm:~1.0.2": + version: 1.0.2 + resolution: "level-fix-range@npm:1.0.2" + checksum: 6c9a3894ea08947fae79c41b75e8b9d57979523b656bec43c589f2dc4455276a150df445d9a7ca880a7c58c2ef19f5cea7f661d777993b870f4943af6b31d5bb + languageName: node + linkType: hard + +"level-hooks@npm:>=4.4.0 <5": + version: 4.5.0 + resolution: "level-hooks@npm:4.5.0" + dependencies: + string-range: ~1.2 + checksum: f198ad2e0901a4719e324e67f546097589af79665ebaaabee7122fda18a41ada3158bb1816b8b82430f30c68610125e4e20b5c09ec3ba7ae262d97dba34f48ab + languageName: node + linkType: hard + +"level-js@npm:^2.1.3": + version: 2.2.4 + resolution: "level-js@npm:2.2.4" + dependencies: + abstract-leveldown: ~0.12.0 + idb-wrapper: ^1.5.0 + isbuffer: ~0.0.0 + ltgt: ^2.1.2 + typedarray-to-buffer: ~1.0.0 + xtend: ~2.1.2 + checksum: 4fed784fcfad4bc6ec97d9c3897e95eaa30326fcdab9f4c7437624d10fa875fa84aafcc2acac0d53181af506cbc012c03f413b4da12ff83758d3bcbb699f8c8e + languageName: node + linkType: hard + +"level-peek@npm:1.0.6, level-peek@npm:^1.0.6": + version: 1.0.6 + resolution: "level-peek@npm:1.0.6" + dependencies: + level-fix-range: ~1.0.2 + checksum: e07d5f8b80675727204d9a226a249139da9e354e633b9d57b7a5186a7b85be445e550ca628f5133bf7a220a9311a193ded5a3f83588dc4eaa53ffb86b426154a + languageName: node + linkType: hard + +"level-sublevel@npm:^5.2.0": + version: 5.2.3 + resolution: "level-sublevel@npm:5.2.3" + dependencies: + level-fix-range: 2.0 + level-hooks: ">=4.4.0 <5" + string-range: ~1.2.1 + xtend: ~2.0.4 + checksum: f0fdffc2f9ca289aa183a1bf7f300a8f92e4f01be60eab37ab36e1f6ec33ed449519d8f69504a616e82f3ddca13a15fa4e19af1dcc1beba9044a4c60b6cd94bf + languageName: node + linkType: hard + +"levelup@npm:^0.18.2": + version: 0.18.6 + resolution: "levelup@npm:0.18.6" + dependencies: + bl: ~0.8.1 + deferred-leveldown: ~0.2.0 + errno: ~0.1.1 + prr: ~0.0.0 + readable-stream: ~1.0.26 + semver: ~2.3.1 + xtend: ~3.0.0 + checksum: 80e140dd83dc94050e283fc02874ae85116cb560d81e14fee0ac111f86006887835ec905dca7a081414c07eca202245a580f1e02f696367b777ecc23a9e05b86 + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -27399,6 +28192,13 @@ __metadata: languageName: node linkType: hard +"ltgt@npm:^2.1.2": + version: 2.2.1 + resolution: "ltgt@npm:2.2.1" + checksum: 7e3874296f7538bc8087b428ac4208008d7b76916354b34a08818ca7c83958c1df10ec427eeeaad895f6b81e41e24745b18d30f89abcc21d228b94f6961d50a2 + languageName: node + linkType: hard + "lucide-react@npm:^0.125.0": version: 0.125.0 resolution: "lucide-react@npm:0.125.0" @@ -27451,6 +28251,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.26.7": + version: 0.26.7 + resolution: "magic-string@npm:0.26.7" + dependencies: + sourcemap-codec: ^1.4.8 + checksum: 89b0d60cbb32bbf3d1e23c46ea93db082d18a8230b972027aecb10a40bba51be519ecce0674f995571e3affe917b76b09f59d8dbc9a1b2c9c4102a2b6e8a2b01 + languageName: node + linkType: hard + "magic-string@npm:^0.27.0": version: 0.27.0 resolution: "magic-string@npm:0.27.0" @@ -29335,6 +30144,31 @@ __metadata: languageName: node linkType: hard +"next-auth@npm:^4.10.3": + version: 4.22.0 + resolution: "next-auth@npm:4.22.0" + dependencies: + "@babel/runtime": ^7.20.13 + "@panva/hkdf": ^1.0.2 + cookie: ^0.5.0 + jose: ^4.11.4 + oauth: ^0.9.15 + openid-client: ^5.4.0 + preact: ^10.6.3 + preact-render-to-string: ^5.1.19 + uuid: ^8.3.2 + peerDependencies: + next: ^12.2.5 || ^13 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 + react-dom: ^17.0.2 || ^18 + peerDependenciesMeta: + nodemailer: + optional: true + checksum: 327a7715a963c890afd0b5a47316ce2c4cec887a86d96f2c208135df0f10b52c6e1fdcbb436a77926c24695165beea3bd0588da2411d5dec12fa19baae0c2533 + languageName: node + linkType: hard + "next-auth@npm:^4.20.1": version: 4.20.1 resolution: "next-auth@npm:4.20.1" @@ -29360,6 +30194,17 @@ __metadata: languageName: node linkType: hard +"next-axiom@npm:^0.10.0": + version: 0.10.0 + resolution: "next-axiom@npm:0.10.0" + dependencies: + whatwg-fetch: ^3.6.2 + peerDependencies: + next: ^12.1.4 + checksum: 57aea4edbdab1a4da59eb16644e850a082a23657128f1f953a2b3601b09fa4558090d42b616f8a3e6022c0deef0d95937321f373caa9ca591178dd8445528921 + languageName: node + linkType: hard + "next-axiom@npm:^0.16.0": version: 0.16.0 resolution: "next-axiom@npm:0.16.0" @@ -29467,6 +30312,16 @@ __metadata: languageName: node linkType: hard +"next-transpile-modules@npm:^9.0.0": + version: 9.1.0 + resolution: "next-transpile-modules@npm:9.1.0" + dependencies: + enhanced-resolve: ^5.10.0 + escalade: ^3.1.1 + checksum: 8cc46196db3c2d2063fb29fe5b4d03c21065ab08130085b24d61e4ed512d99c12083d28179771cf02f70f8bf5970db0781a228aacf6cc61662dbdcabaddfc472 + languageName: node + linkType: hard + "next-validations@npm:^0.2.0": version: 0.2.1 resolution: "next-validations@npm:0.2.1" @@ -29547,6 +30402,75 @@ __metadata: languageName: node linkType: hard +"next@npm:^12.3.1": + version: 12.3.4 + resolution: "next@npm:12.3.4" + dependencies: + "@next/env": 12.3.4 + "@next/swc-android-arm-eabi": 12.3.4 + "@next/swc-android-arm64": 12.3.4 + "@next/swc-darwin-arm64": 12.3.4 + "@next/swc-darwin-x64": 12.3.4 + "@next/swc-freebsd-x64": 12.3.4 + "@next/swc-linux-arm-gnueabihf": 12.3.4 + "@next/swc-linux-arm64-gnu": 12.3.4 + "@next/swc-linux-arm64-musl": 12.3.4 + "@next/swc-linux-x64-gnu": 12.3.4 + "@next/swc-linux-x64-musl": 12.3.4 + "@next/swc-win32-arm64-msvc": 12.3.4 + "@next/swc-win32-ia32-msvc": 12.3.4 + "@next/swc-win32-x64-msvc": 12.3.4 + "@swc/helpers": 0.4.11 + caniuse-lite: ^1.0.30001406 + postcss: 8.4.14 + styled-jsx: 5.0.7 + use-sync-external-store: 1.2.0 + peerDependencies: + fibers: ">= 3.1.0" + node-sass: ^6.0.0 || ^7.0.0 + react: ^17.0.2 || ^18.0.0-0 + react-dom: ^17.0.2 || ^18.0.0-0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-android-arm-eabi": + optional: true + "@next/swc-android-arm64": + optional: true + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-freebsd-x64": + optional: true + "@next/swc-linux-arm-gnueabihf": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-ia32-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: d96fc4f5bcd5a630d74111519f4820dcbd75dddf16c6d00d2167bd3cb8d74965d46d83c8e5ec301bf999013c7d96f1bfff9424f0221317d68b594c4d01f5825e + languageName: node + linkType: hard + "next@npm:^13.2.1": version: 13.2.3 resolution: "next@npm:13.2.3" @@ -30037,13 +30961,6 @@ __metadata: languageName: node linkType: hard -"oauth4webapi@npm:2.0.5": - version: 2.0.5 - resolution: "oauth4webapi@npm:2.0.5" - checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9 - languageName: node - linkType: hard - "oauth@npm:^0.9.15": version: 0.9.15 resolution: "oauth@npm:0.9.15" @@ -30104,6 +31021,24 @@ __metadata: languageName: node linkType: hard +"object-keys@npm:~0.2.0": + version: 0.2.0 + resolution: "object-keys@npm:0.2.0" + dependencies: + foreach: ~2.0.1 + indexof: ~0.0.1 + is: ~0.2.6 + checksum: 4b96bab88fe9df22a03aec3c59a084bdffc789ad1318a39081e6b8389af6b9ab8571dd3776eed3ec5831137d057fb7ba76911552c6a6efd59b5d126ac3b6e432 + languageName: node + linkType: hard + +"object-keys@npm:~0.4.0": + version: 0.4.0 + resolution: "object-keys@npm:0.4.0" + checksum: 1be3ebe9b48c0d5eda8e4a30657d887a748cb42435e0e2eaf49faf557bdd602cd2b7558b8ce90a4eb2b8592d16b875a1900bce859cbb0f35b21c67e11a45313c + languageName: node + linkType: hard + "object-path@npm:^0.11.8": version: 0.11.8 resolution: "object-path@npm:0.11.8" @@ -30267,6 +31202,13 @@ __metadata: languageName: node linkType: hard +"octal@npm:^1.0.0": + version: 1.0.0 + resolution: "octal@npm:1.0.0" + checksum: d648917f4f0a1042d7a4e230262aed00274c9791fe4795e9a2ce3b64ab7f2ca93e62cd55ca5ad4e4bd3fc375ca84d6919d7bf417be461790c1042503ac2c2310 + languageName: node + linkType: hard + "oidc-token-hash@npm:^5.0.1": version: 5.0.1 resolution: "oidc-token-hash@npm:5.0.1" @@ -31709,17 +32651,6 @@ __metadata: languageName: node linkType: hard -"preact-render-to-string@npm:5.2.3": - version: 5.2.3 - resolution: "preact-render-to-string@npm:5.2.3" - dependencies: - pretty-format: ^3.8.0 - peerDependencies: - preact: ">=10" - checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44 - languageName: node - linkType: hard - "preact-render-to-string@npm:^5.1.19": version: 5.2.6 resolution: "preact-render-to-string@npm:5.2.6" @@ -31731,13 +32662,6 @@ __metadata: languageName: node linkType: hard -"preact@npm:10.11.3, preact@npm:^10.6.3": - version: 10.11.3 - resolution: "preact@npm:10.11.3" - checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367 - languageName: node - linkType: hard - "preact@npm:10.4.1": version: 10.4.1 resolution: "preact@npm:10.4.1" @@ -31752,6 +32676,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.6.3": + version: 10.11.3 + resolution: "preact@npm:10.11.3" + checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -31976,6 +32907,18 @@ __metadata: languageName: node linkType: hard +"prisma@npm:^4.7.1": + version: 4.12.0 + resolution: "prisma@npm:4.12.0" + dependencies: + "@prisma/engines": 4.12.0 + bin: + prisma: build/index.js + prisma2: build/index.js + checksum: 826b90901391eead0aa2e1ab4539474c308f8a956b480e87594b241bfaba423ebd781b8082fc329f4e7e780f3a7162a09ac61c367d3f43382e93d506e4a66c7c + languageName: node + linkType: hard + "prismjs@npm:^1.27.0": version: 1.28.0 resolution: "prismjs@npm:1.28.0" @@ -31990,6 +32933,13 @@ __metadata: languageName: node linkType: hard +"process-es6@npm:^0.11.2": + version: 0.11.6 + resolution: "process-es6@npm:0.11.6" + checksum: 8849ea1a799a20a8e863fd3a5558d4085357ee59cae16b76f61327e3b3a27697b4e49c880742a6cc0f0c37eb0bd78fb0d4e382bd2e5318bb699b404b55a9b91e + languageName: node + linkType: hard + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -32130,6 +33080,13 @@ __metadata: languageName: node linkType: hard +"prr@npm:~0.0.0": + version: 0.0.0 + resolution: "prr@npm:0.0.0" + checksum: 6552d9d92d9d55ec1afb8952ad80f81bbb1b4379f24ff7c506ad083ea701caf1bf6d4b092a2baeb98ec3f312c5a49d8bdf1d9b20a6db2998d05c2d52aa6a82e7 + languageName: node + linkType: hard + "prr@npm:~1.0.1": version: 1.0.1 resolution: "prr@npm:1.0.1" @@ -32768,6 +33725,17 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^3.1.0": + version: 3.1.4 + resolution: "react-error-boundary@npm:3.1.4" + dependencies: + "@babel/runtime": ^7.12.5 + peerDependencies: + react: ">=16.13.1" + checksum: f36270a5d775a25c8920f854c0d91649ceea417b15b5bc51e270a959b0476647bb79abb4da3be7dd9a4597b029214e8fe43ea914a7f16fa7543c91f784977f1b + languageName: node + linkType: hard + "react-fast-marquee@npm:^1.3.5": version: 1.3.5 resolution: "react-fast-marquee@npm:1.3.5" @@ -32814,6 +33782,15 @@ __metadata: languageName: node linkType: hard +"react-hook-form@npm:^7.34.2": + version: 7.43.9 + resolution: "react-hook-form@npm:7.43.9" + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + checksum: 65b94de625f2b7921c4e856bf0abbe142bfe06c052217bd1bcc3a842e2cc37fa3a3e03758119dc038bbcf5edb49e02c29206528b80b201f9a4d601471ef78153 + languageName: node + linkType: hard + "react-hook-form@npm:^7.43.3": version: 7.43.3 resolution: "react-hook-form@npm:7.43.3" @@ -33240,6 +34217,15 @@ __metadata: languageName: node linkType: hard +"react-sticky-box@npm:^2.0.4": + version: 2.0.4 + resolution: "react-sticky-box@npm:2.0.4" + peerDependencies: + react: ">=16.8.0" + checksum: 6f6c1ab7b04851bcafce12cf9125d60d7944ff4f713ecfa53babb7695c6bfbb66d89a3e3cb040145a895a49a0fc9d47cf7325de3402a95f7fb16899ea96f2f80 + languageName: node + linkType: hard + "react-string-replace@npm:^1.1.0": version: 1.1.0 resolution: "react-string-replace@npm:1.1.0" @@ -33490,6 +34476,18 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^1.0.26-4": + version: 1.1.14 + resolution: "readable-stream@npm:1.1.14" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.1 + isarray: 0.0.1 + string_decoder: ~0.10.x + checksum: 17dfeae3e909945a4a1abc5613ea92d03269ef54c49288599507fc98ff4615988a1c39a999dcf9aacba70233d9b7040bc11a5f2bfc947e262dedcc0a8b32b5a0 + languageName: node + linkType: hard + "readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" @@ -33501,6 +34499,18 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:~1.0.26, readable-stream@npm:~1.0.26-4": + version: 1.0.34 + resolution: "readable-stream@npm:1.0.34" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.1 + isarray: 0.0.1 + string_decoder: ~0.10.x + checksum: 85042c537e4f067daa1448a7e257a201070bfec3dd2706abdbd8ebc7f3418eb4d3ed4b8e5af63e2544d69f88ab09c28d5da3c0b77dc76185fddd189a59863b60 + languageName: node + linkType: hard + "readdirp@npm:^2.2.1": version: 2.2.1 resolution: "readdirp@npm:2.2.1" @@ -34348,6 +35358,18 @@ __metadata: languageName: node linkType: hard +"rollup-plugin-node-builtins@npm:^2.1.2": + version: 2.1.2 + resolution: "rollup-plugin-node-builtins@npm:2.1.2" + dependencies: + browserify-fs: ^1.0.0 + buffer-es6: ^4.9.2 + crypto-browserify: ^3.11.0 + process-es6: ^0.11.2 + checksum: 184338123fff678e1671ef958621058b679b5bc955620bb4457fe7e7005c6550497b9d978f7f74705ea46a6adedaa541066d3c7454c6878bde48791cd9a7b61a + languageName: node + linkType: hard + "rollup-plugin-polyfill-node@npm:^0.10.2": version: 0.10.2 resolution: "rollup-plugin-polyfill-node@npm:0.10.2" @@ -34387,6 +35409,20 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^2.79.1": + version: 2.79.1 + resolution: "rollup@npm:2.79.1" + dependencies: + fsevents: ~2.3.2 + dependenciesMeta: + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 6a2bf167b3587d4df709b37d149ad0300692cc5deb510f89ac7bdc77c8738c9546ae3de9322b0968e1ed2b0e984571f5f55aae28fa7de4cfcb1bc5402a4e2be6 + languageName: node + linkType: hard + "rollup@npm:^3.10.0": version: 3.17.0 resolution: "rollup@npm:3.17.0" @@ -34870,6 +35906,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:~2.3.1": + version: 2.3.2 + resolution: "semver@npm:2.3.2" + bin: + semver: ./bin/semver + checksum: e0649fb18a1da909df7b5a6f586314a7f6e052385fc1e6eafa7084dd77c0787e755ab35ca491f9eec986fe1d0d6d36eae85a21eb7e2ed32ae5906796acb92c56 + languageName: node + linkType: hard + "send@npm:0.17.2": version: 0.17.2 resolution: "send@npm:0.17.2" @@ -35911,6 +36956,13 @@ __metadata: languageName: node linkType: hard +"string-range@npm:~1.2, string-range@npm:~1.2.1": + version: 1.2.2 + resolution: "string-range@npm:1.2.2" + checksum: 7118cc83a7e63fca5fd8bef9b61464bfc51197b5f6dc475c9e1d24a93ce02fa27f7adb4cd7adac5daf599bde442b383608078f9b051bddb108d3b45840923097 + languageName: node + linkType: hard + "string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -36056,6 +37108,13 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~0.10.x": + version: 0.10.31 + resolution: "string_decoder@npm:0.10.31" + checksum: fe00f8e303647e5db919948ccb5ce0da7dea209ab54702894dd0c664edd98e5d4df4b80d6fabf7b9e92b237359d21136c95bf068b2f7760b772ca974ba970202 + languageName: node + linkType: hard + "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -36305,6 +37364,20 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.0.7": + version: 5.0.7 + resolution: "styled-jsx@npm:5.0.7" + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 61959993915f4b1662a682dbbefb3512de9399cf6901969bcadd26ba5441d2b5ca5c1021b233bbd573da2541b41efb45d56c6f618dbc8d88a381ebc62461fefe + languageName: node + linkType: hard + "styled-jsx@npm:5.1.1": version: 5.1.1 resolution: "styled-jsx@npm:5.1.1" @@ -37683,6 +38756,13 @@ __metadata: languageName: node linkType: hard +"turbo-darwin-64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-darwin-64@npm:1.9.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "turbo-darwin-arm64@npm:1.8.3": version: 1.8.3 resolution: "turbo-darwin-arm64@npm:1.8.3" @@ -37690,6 +38770,13 @@ __metadata: languageName: node linkType: hard +"turbo-darwin-arm64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-darwin-arm64@npm:1.9.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "turbo-linux-64@npm:1.8.3": version: 1.8.3 resolution: "turbo-linux-64@npm:1.8.3" @@ -37697,6 +38784,13 @@ __metadata: languageName: node linkType: hard +"turbo-linux-64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-linux-64@npm:1.9.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "turbo-linux-arm64@npm:1.8.3": version: 1.8.3 resolution: "turbo-linux-arm64@npm:1.8.3" @@ -37704,6 +38798,13 @@ __metadata: languageName: node linkType: hard +"turbo-linux-arm64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-linux-arm64@npm:1.9.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "turbo-windows-64@npm:1.8.3": version: 1.8.3 resolution: "turbo-windows-64@npm:1.8.3" @@ -37711,6 +38812,13 @@ __metadata: languageName: node linkType: hard +"turbo-windows-64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-windows-64@npm:1.9.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "turbo-windows-arm64@npm:1.8.3": version: 1.8.3 resolution: "turbo-windows-arm64@npm:1.8.3" @@ -37718,6 +38826,42 @@ __metadata: languageName: node linkType: hard +"turbo-windows-arm64@npm:1.9.3": + version: 1.9.3 + resolution: "turbo-windows-arm64@npm:1.9.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"turbo@npm:^1.4.3": + version: 1.9.3 + resolution: "turbo@npm:1.9.3" + dependencies: + turbo-darwin-64: 1.9.3 + turbo-darwin-arm64: 1.9.3 + turbo-linux-64: 1.9.3 + turbo-linux-arm64: 1.9.3 + turbo-windows-64: 1.9.3 + turbo-windows-arm64: 1.9.3 + dependenciesMeta: + turbo-darwin-64: + optional: true + turbo-darwin-arm64: + optional: true + turbo-linux-64: + optional: true + turbo-linux-arm64: + optional: true + turbo-windows-64: + optional: true + turbo-windows-arm64: + optional: true + bin: + turbo: bin/turbo + checksum: ebf06d3b9b1401a5baabace238cd1e0d8fc1dc062b4f7bd577f644298c555f326d15f331144641c0b43a60ae8058769bcbd9d1660874fa9927ec64b5be8ee9dc + languageName: node + linkType: hard + "turbo@npm:^1.8.3": version: 1.8.3 resolution: "turbo@npm:1.8.3" @@ -37943,6 +39087,13 @@ __metadata: languageName: node linkType: hard +"typedarray-to-buffer@npm:~1.0.0": + version: 1.0.4 + resolution: "typedarray-to-buffer@npm:1.0.4" + checksum: ac6989c456a0b175c8362b3ebbd8a74af7b9bcc94f9dc9ffd34436569cd29aea6a1e0e5f5752d0d5bd855a55b2520e960d1d4cb9c9149f863ce09220540df17f + languageName: node + linkType: hard + "typedarray@npm:^0.0.6": version: 0.0.6 resolution: "typedarray@npm:0.0.6" @@ -38032,6 +39183,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^4.7.4, typescript@npm:^4.9.3": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + languageName: node + linkType: hard + "typescript@npm:^4.9.4": version: 4.9.4 resolution: "typescript@npm:4.9.4" @@ -38042,6 +39203,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>, typescript@patch:typescript@^4.9.3#~builtin<compat/typescript>": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=23ec76" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d + languageName: node + linkType: hard + "typescript@patch:typescript@^4.9.4#~builtin<compat/typescript>": version: 4.9.4 resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=23ec76" @@ -39089,6 +40260,44 @@ __metadata: languageName: node linkType: hard +"vite@npm:^3.2.4": + version: 3.2.5 + resolution: "vite@npm:3.2.5" + dependencies: + esbuild: ^0.15.9 + fsevents: ~2.3.2 + postcss: ^8.4.18 + resolve: ^1.22.1 + rollup: ^2.79.1 + peerDependencies: + "@types/node": ">= 14" + less: "*" + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: ad35b7008c2b62a167d1d1a82f0a0c60fa457733f1969e9eedf0b0077f67a7ac74b4c9477e75a397895150f09b6551f0c17841c5b05c34d9fe302bb0b5dc28a8 + languageName: node + linkType: hard + "vite@npm:^4.1.2": version: 4.1.2 resolution: "vite@npm:4.1.2" @@ -40339,6 +41548,13 @@ __metadata: languageName: node linkType: hard +"xtend@npm:^2.2.0": + version: 2.2.0 + resolution: "xtend@npm:2.2.0" + checksum: 9fcd1ddabefdb3c68a698b08177525ad14a6df3423b13bad9a53900d19374e476a43c219b0756d39675776b2326a35fe477c547cfb8a05ae9fea4ba2235bebe2 + languageName: node + linkType: hard + "xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" @@ -40346,6 +41562,32 @@ __metadata: languageName: node linkType: hard +"xtend@npm:~2.0.4": + version: 2.0.6 + resolution: "xtend@npm:2.0.6" + dependencies: + is-object: ~0.1.2 + object-keys: ~0.2.0 + checksum: 414531e51cbc56d4676ae2b3a4070052e0c7a36caf7ee74f2e8449fe0fc1752b971a776fca5b85ec02ef3d0a33b8e75491d900474b8407f3f4bba3f49325a785 + languageName: node + linkType: hard + +"xtend@npm:~2.1.2": + version: 2.1.2 + resolution: "xtend@npm:2.1.2" + dependencies: + object-keys: ~0.4.0 + checksum: a8b79f31502c163205984eaa2b196051cd2fab0882b49758e30f2f9018255bc6c462e32a090bf3385d1bda04755ad8cc0052a09e049b0038f49eb9b950d9c447 + languageName: node + linkType: hard + +"xtend@npm:~3.0.0": + version: 3.0.0 + resolution: "xtend@npm:3.0.0" + checksum: ecdc4dd74f26e561dbc13d4148fcc7b8f46f49b9259862fc31e42b7cede9eee62af9d869050a7b8e089475e858744a74ceae3f0da2943755ef712f3277ad2e50 + languageName: node + linkType: hard + "y18n@npm:^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3" @@ -40631,7 +41873,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.17.3": +"zod@npm:^3.17.3, zod@npm:^3.19.1": version: 3.21.4 resolution: "zod@npm:3.21.4" checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f @@ -40662,9 +41904,9 @@ __metadata: languageName: node linkType: hard -"zustand@npm:^4.1.4": - version: 4.1.5 - resolution: "zustand@npm:4.1.5" +"zustand@npm:^4.3.2": + version: 4.3.6 + resolution: "zustand@npm:4.3.6" dependencies: use-sync-external-store: 1.2.0 peerDependencies: @@ -40675,7 +41917,7 @@ __metadata: optional: true react: optional: true - checksum: 13190ee8e8a797c5347b525a7c392be62b2addacdd9645dd20d37ea053f96c7c7067c099c6201e98ebb8d54991f2e04e241cc323f9a25b841d44f0ae048e3afc + checksum: 4d3cec03526f04ff3de6dc45b6f038c47f091836af9660fbf5f682cae1628221102882df20e4048dfe699a43f67424e5d6afc1116f3838a80eea5dd4f95ddaed languageName: node linkType: hard