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