Merge branch 'main' into feat/impersonation-improvements

This commit is contained in:
sean-brydon 2024-01-12 20:35:28 +13:00 committed by GitHub
commit 805ddfaf87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1147 additions and 460 deletions

View File

@ -28,7 +28,7 @@ jobs:
with:
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
organization-name: calcom
ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
ignore-labels: "admin, app-store, ai, authentication, automated-testing, devops, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
apply-labels-from-issue:
runs-on: ubuntu-latest

View File

@ -69,7 +69,7 @@ jobs:
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo "{body}={$body}" >> $GITHUB_OUTPUT
echo "{body}=${body}" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v2

View File

@ -0,0 +1,57 @@
import type { GetServerSidePropsContext } from "next";
import { isNotFoundError } from "next/dist/client/components/not-found";
import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
import { notFound, redirect } from "next/navigation";
import { WEBAPP_URL } from "@calcom/lib/constants";
export type EmbedProps = {
isEmbed?: boolean;
};
export default function withEmbedSsrAppDir<T extends Record<string, any>>(
getData: (context: GetServerSidePropsContext) => Promise<T>
) {
return async (context: GetServerSidePropsContext): Promise<T> => {
const { embed, layout } = context.query;
try {
const props = await getData(context);
return {
...props,
isEmbed: true,
};
} catch (e) {
if (isRedirectError(e)) {
const destinationUrl = getURLFromRedirectError(e);
let urlPrefix = "";
// Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
const destinationUrlObj = new URL(destinationUrl, WEBAPP_URL);
// If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
urlPrefix = destinationUrlObj.origin;
} else {
// Don't use any prefix for relative URLs to ensure we stay on the same domain
urlPrefix = "";
}
const destinationQueryStr = destinationUrlObj.searchParams.toString();
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
destinationQueryStr ? `${destinationQueryStr}&` : ""
}layout=${layout}&embed=${embed}`;
redirect(newDestinationUrl);
}
if (isNotFoundError(e)) {
notFound();
}
throw e;
}
};
}

View File

@ -0,0 +1,10 @@
import OldPage from "@pages/booking/[uid]";
import withEmbedSsrAppDir from "app/WithEmbedSSR";
import { WithLayout } from "app/layoutHOC";
import { getData } from "../page";
const getEmbedData = withEmbedSsrAppDir(getData);
// @ts-expect-error Type '(context: GetServerSidePropsContext) => Promise<any>' is not assignable to type '(arg: {
export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });

View File

@ -0,0 +1,204 @@
import OldPage from "@pages/booking/[uid]";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import type { GetServerSidePropsContext } from "next";
import { notFound } from "next/navigation";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } from "@lib/booking";
import { ssrInit } from "@server/lib/ssr";
const stringToBoolean = z
.string()
.optional()
.transform((val) => val === "true");
const querySchema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(),
seatReferenceUid: z.string().optional(),
});
export const generateMetadata = async () =>
await _generateMetadata(
() => "",
() => ""
);
export const getData = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getServerSession(context);
let tz: string | null = null;
let userTimeFormat: number | null = null;
let requiresLoginToUpdate = false;
if (session) {
const user = await ssr.viewer.me.fetch();
tz = user.timeZone;
userTimeFormat = user.timeFormat;
}
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) {
notFound();
}
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
uid: maybeUid,
},
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
metadata: true,
cancellationReason: true,
responses: true,
rejectionReason: true,
user: {
select: {
id: true,
name: true,
email: true,
username: true,
timeZone: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
eventTypeId: true,
eventType: {
select: {
eventName: true,
slug: true,
timeZone: true,
},
},
seatsReferences: {
select: {
referenceUid: true,
},
},
},
});
if (!bookingInfoRaw) {
notFound();
}
const eventTypeRaw = !bookingInfoRaw.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
if (!eventTypeRaw) {
notFound();
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
requiresLoginToUpdate = true;
}
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
? eventTypeRaw.hosts.map((host) => host.user)
: eventTypeRaw.users;
if (!eventTypeRaw.users.length) {
if (!eventTypeRaw.owner) {
notFound();
}
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
}
const eventType = {
...eventTypeRaw,
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
}
const payment = await prisma.payment.findFirst({
where: {
bookingId: bookingInfo.id,
},
select: {
success: true,
refunded: true,
currency: true,
amount: true,
paymentOption: true,
},
});
return {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
dehydratedState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
paymentStatus: payment,
...(tz && { tz }),
userTimeFormat,
requiresLoginToUpdate,
};
};
// @ts-expect-error Argument of type '{ req: { headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }; }' is not assignable to parameter of type 'GetServerSidePropsContext'.
export default WithLayout({ getLayout: null, getData, Page: OldPage });

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
export default WithLayout({ getLayout })<"L">;

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import EnterprisePage from "@components/EnterprisePage";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("create_your_org"),
(t) => t("create_your_org_description")
);
export default EnterprisePage;

View File

@ -0,0 +1,26 @@
import LegacyPage from "@pages/insights/index";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { notFound } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
export const generateMetadata = async () =>
await _generateMetadata(
() => "Insights",
(t) => t("insights_subtitle")
);
async function getData() {
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const flags = await getFeatureFlagMap(prisma);
if (flags.insights === false) {
return notFound();
}
return {};
}
export default WithLayout({ getLayout, getData, Page: LegacyPage });

View File

@ -0,0 +1,4 @@
import Page from "@pages/more";
import { WithLayout } from "app/layoutHOC";
export default WithLayout({ getLayout: null, Page })<"P">;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/my-account/appearance";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("appearance"),
(t) => t("appearance_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/my-account/calendars";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("calendars"),
(t) => t("calendars_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/my-account/conferencing";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("conferencing"),
(t) => t("conferencing_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/my-account/general";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("general"),
(t) => t("general_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/my-account/profile";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("profile"),
(t) => t("profile_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/security/impersonation";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("impersonation"),
(t) => t("impersonation_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/security/password";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("password"),
(t) => t("password_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,11 @@
import { _generateMetadata } from "app/_utils";
import Page from "@calcom/features/ee/sso/page/user-sso-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("sso_configuration"),
(t) => t("sso_configuration_description")
);
export default Page;

View File

@ -0,0 +1,5 @@
import { WithLayout } from "app/layoutHOC";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
export default WithLayout({ getLayout });

View File

@ -0,0 +1,10 @@
import Page from "@pages/settings/security/two-factor-auth";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("two_factor_auth"),
(t) => t("add_an_extra_layer_of_security")
);
export default Page;

View File

@ -0,0 +1,70 @@
"use client";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, ButtonGroup } from "@calcom/ui";
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
export default function EnterprisePage() {
const { t } = useLocale();
const features = [
{
icon: <Globe className="h-5 w-5 text-red-500" />,
title: t("branded_subdomain"),
description: t("branded_subdomain_description"),
},
{
icon: <BarChart className="h-5 w-5 text-blue-500" />,
title: t("org_insights"),
description: t("org_insights_description"),
},
{
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
title: t("extensive_whitelabeling"),
description: t("extensive_whitelabeling_description"),
},
{
icon: <Users className="h-5 w-5 text-orange-500" />,
title: t("unlimited_teams"),
description: t("unlimited_teams_description"),
},
{
icon: <CreditCard className="h-5 w-5 text-green-500" />,
title: t("unified_billing"),
description: t("unified_billing_description"),
},
{
icon: <Lock className="h-5 w-5 text-purple-500" />,
title: t("advanced_managed_events"),
description: t("advanced_managed_events_description"),
},
];
return (
<div>
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
<UpgradeTip
plan="enterprise"
title={t("create_your_org")}
description={t("create_your_org_description")}
features={features}
background="/tips/enterprise"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href="https://i.cal.com/sales/enterprise?duration=25" target="_blank">
{t("contact_sales")}
</Button>
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<>Create Org</>
</UpgradeTip>
</ShellMain>
</div>
);
}

View File

@ -116,7 +116,7 @@ function getNavigation(props: {
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
icon: PhoneCall,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
];
@ -219,7 +219,7 @@ function EventTypeSingleLayout({
navigation.push({
name: "instant_tab_title",
href: `/event-types/${eventType.id}?tabName=instant`,
icon: Zap,
icon: PhoneCall,
info: `instant_event_tab_description`,
});
}

170
apps/web/lib/booking.ts Normal file
View File

@ -0,0 +1,170 @@
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
export const getEventTypesFromDB = async (id: number) => {
const userSelect = {
id: true,
name: true,
username: true,
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
};
const eventType = await prisma.eventType.findUnique({
where: {
id,
},
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
recurringEvent: true,
requiresConfirmation: true,
userId: true,
successRedirectUrl: true,
customInputs: true,
locations: true,
price: true,
currency: true,
bookingFields: true,
disableGuests: true,
timeZone: true,
owner: {
select: userSelect,
},
users: {
select: userSelect,
},
hosts: {
select: {
user: {
select: userSelect,
},
},
},
team: {
select: {
slug: true,
name: true,
hideBranding: true,
},
},
workflows: {
select: {
workflow: {
select: {
id: true,
steps: true,
},
},
},
},
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodStartDate: true,
periodEndDate: true,
},
});
if (!eventType) {
return eventType;
}
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
return {
isDynamic: false,
...eventType,
bookingFields: getBookingFieldsWithSystemFields(eventType),
metadata,
};
};
export const handleSeatsEventTypeOnBooking = async (
eventType: {
seatsPerTimeSlot?: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
[x: string | number | symbol]: unknown;
},
bookingInfo: Partial<
Prisma.BookingGetPayload<{
include: {
attendees: { select: { name: true; email: true } };
seatsReferences: { select: { referenceUid: true } };
user: {
select: {
id: true;
name: true;
email: true;
username: true;
timeZone: true;
};
};
};
}>
>,
seatReferenceUid?: string,
userId?: number
) => {
if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete bookingInfo.description;
} else {
return;
}
// @TODO: If handling teams, we need to do more check ups for this.
if (bookingInfo?.user?.id === userId) {
return;
}
if (!eventType.seatsShowAttendees) {
const seatAttendee = await prisma.bookingSeat.findFirst({
where: {
referenceUid: seatReferenceUid,
},
include: {
attendee: {
select: {
name: true,
email: true,
},
},
},
});
if (seatAttendee) {
const attendee = bookingInfo?.attendees?.find((a) => {
return a.email === seatAttendee.attendee?.email;
});
bookingInfo["attendees"] = attendee ? [attendee] : [];
} else {
bookingInfo["attendees"] = [];
}
}
return bookingInfo;
};
export async function getRecurringBookings(recurringEventId: string | null) {
if (!recurringEventId) return null;
const recurringBookings = await prisma.booking.findMany({
where: {
recurringEventId,
status: BookingStatus.ACCEPTED,
},
select: {
startTime: true,
},
});
return recurringBookings.map((obj) => obj.startTime.toString());
}

View File

@ -0,0 +1,199 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { ssrInit } from "@server/lib/ssr";
const stringToBoolean = z
.string()
.optional()
.transform((val) => val === "true");
const querySchema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(),
seatReferenceUid: z.string().optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
// this is needed to prevent bundling of lib/booking to the client bundle
// usually functions that are used in getServerSideProps are tree shaken from client bundle
// but not in case when they are exported. So we have to dynamically load them, or to copy paste them to the /future/page.
const { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } = await import(
"@lib/booking"
);
const ssr = await ssrInit(context);
const session = await getServerSession(context);
let tz: string | null = null;
let userTimeFormat: number | null = null;
let requiresLoginToUpdate = false;
if (session) {
const user = await ssr.viewer.me.fetch();
tz = user.timeZone;
userTimeFormat = user.timeFormat;
}
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true } as const;
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
uid: maybeUid,
},
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
metadata: true,
cancellationReason: true,
responses: true,
rejectionReason: true,
user: {
select: {
id: true,
name: true,
email: true,
username: true,
timeZone: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
eventTypeId: true,
eventType: {
select: {
eventName: true,
slug: true,
timeZone: true,
},
},
seatsReferences: {
select: {
referenceUid: true,
},
},
},
});
if (!bookingInfoRaw) {
return {
notFound: true,
} as const;
}
const eventTypeRaw = !bookingInfoRaw.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
if (!eventTypeRaw) {
return {
notFound: true,
} as const;
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
requiresLoginToUpdate = true;
}
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
? eventTypeRaw.hosts.map((host) => host.user)
: eventTypeRaw.users;
if (!eventTypeRaw.users.length) {
if (!eventTypeRaw.owner)
return {
notFound: true,
} as const;
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
}
const eventType = {
...eventTypeRaw,
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
}
const payment = await prisma.payment.findFirst({
where: {
bookingId: bookingInfo.id,
},
select: {
success: true,
refunded: true,
currency: true,
amount: true,
paymentOption: true,
},
});
return {
props: {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
trpcState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
paymentStatus: payment,
...(tz && { tz }),
userTimeFormat,
requiresLoginToUpdate,
},
};
}

View File

@ -39,7 +39,7 @@
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.37.0",
"@formkit/auto-animate": "^0.8.1",
"@formkit/auto-animate": "1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",

View File

@ -18,6 +18,7 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
@ -412,9 +413,9 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
name: user.name || user.username || "",
image: user.avatar,
theme: user.theme,
brandColor: user.brandColor,
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
avatarUrl: user.avatarUrl,
darkBrandColor: user.darkBrandColor,
darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
allowSEOIndexing: user.allowSEOIndexing ?? true,
username: user.username,
organization: {

View File

@ -55,6 +55,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const payload: OAuthTokenPayload = {
userId: decodedRefreshToken.userId,
teamId: decodedRefreshToken.teamId,
scope: decodedRefreshToken.scope,
token_type: "Access Token",
clientId: client_id,

View File

@ -1,7 +1,8 @@
"use client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { createEvent } from "ics";
import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
@ -22,35 +23,28 @@ import {
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import { APP_NAME } from "@calcom/lib/constants";
import {
formatToLocalizedDate,
formatToLocalizedTime,
formatToLocalizedTimezone,
} from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema, customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
import { getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import { timeZone } from "@lib/clock";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
@ -58,23 +52,7 @@ import PageWrapper from "@components/PageWrapper";
import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { ssrInit } from "@server/lib/ssr";
const useBrandColors = ({
brandColor,
darkBrandColor,
}: {
brandColor?: string | null;
darkBrandColor?: string | null;
}) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
export { getServerSideProps };
const stringToBoolean = z
.string()
@ -94,6 +72,22 @@ const querySchema = z.object({
seatReferenceUid: z.string().optional(),
});
const useBrandColors = ({
brandColor,
darkBrandColor,
}: {
brandColor?: string | null;
darkBrandColor?: string | null;
}) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
@ -925,329 +919,3 @@ export function RecurringBookings({
</div>
);
}
const getEventTypesFromDB = async (id: number) => {
const userSelect = {
id: true,
name: true,
username: true,
hideBranding: true,
theme: true,
brandColor: true,
darkBrandColor: true,
email: true,
timeZone: true,
};
const eventType = await prisma.eventType.findUnique({
where: {
id,
},
select: {
id: true,
title: true,
description: true,
length: true,
eventName: true,
recurringEvent: true,
requiresConfirmation: true,
userId: true,
successRedirectUrl: true,
customInputs: true,
locations: true,
price: true,
currency: true,
bookingFields: true,
disableGuests: true,
timeZone: true,
owner: {
select: userSelect,
},
users: {
select: userSelect,
},
hosts: {
select: {
user: {
select: userSelect,
},
},
},
team: {
select: {
slug: true,
name: true,
hideBranding: true,
},
},
workflows: {
select: {
workflow: {
select: {
id: true,
steps: true,
},
},
},
},
metadata: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
periodStartDate: true,
periodEndDate: true,
},
});
if (!eventType) {
return eventType;
}
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
return {
isDynamic: false,
...eventType,
bookingFields: getBookingFieldsWithSystemFields(eventType),
metadata,
};
};
const handleSeatsEventTypeOnBooking = async (
eventType: {
seatsPerTimeSlot?: number | null;
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
[x: string | number | symbol]: unknown;
},
bookingInfo: Partial<
Prisma.BookingGetPayload<{
include: {
attendees: { select: { name: true; email: true } };
seatsReferences: { select: { referenceUid: true } };
user: {
select: {
id: true;
name: true;
email: true;
username: true;
timeZone: true;
};
};
};
}>
>,
seatReferenceUid?: string,
userId?: number
) => {
if (eventType?.seatsPerTimeSlot !== null) {
// @TODO: right now bookings with seats doesn't save every description that its entered by every user
delete bookingInfo.description;
} else {
return;
}
// @TODO: If handling teams, we need to do more check ups for this.
if (bookingInfo?.user?.id === userId) {
return;
}
if (!eventType.seatsShowAttendees) {
const seatAttendee = await prisma.bookingSeat.findFirst({
where: {
referenceUid: seatReferenceUid,
},
include: {
attendee: {
select: {
name: true,
email: true,
},
},
},
});
if (seatAttendee) {
const attendee = bookingInfo?.attendees?.find((a) => {
return a.email === seatAttendee.attendee?.email;
});
bookingInfo["attendees"] = attendee ? [attendee] : [];
} else {
bookingInfo["attendees"] = [];
}
}
return bookingInfo;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const session = await getServerSession(context);
let tz: string | null = null;
let userTimeFormat: number | null = null;
let requiresLoginToUpdate = false;
if (session) {
const user = await ssr.viewer.me.fetch();
tz = user.timeZone;
userTimeFormat = user.timeFormat;
}
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true } as const;
const { uid, eventTypeSlug, seatReferenceUid } = parsedQuery.data;
const { uid: maybeUid } = await maybeGetBookingUidFromSeat(prisma, uid);
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
uid: maybeUid,
},
select: {
title: true,
id: true,
uid: true,
description: true,
customInputs: true,
smsReminderNumber: true,
recurringEventId: true,
startTime: true,
endTime: true,
location: true,
status: true,
metadata: true,
cancellationReason: true,
responses: true,
rejectionReason: true,
user: {
select: {
id: true,
name: true,
email: true,
username: true,
timeZone: true,
},
},
attendees: {
select: {
name: true,
email: true,
timeZone: true,
},
},
eventTypeId: true,
eventType: {
select: {
eventName: true,
slug: true,
timeZone: true,
},
},
seatsReferences: {
select: {
referenceUid: true,
},
},
},
});
if (!bookingInfoRaw) {
return {
notFound: true,
} as const;
}
const eventTypeRaw = !bookingInfoRaw.eventTypeId
? getDefaultEvent(eventTypeSlug || "")
: await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
if (!eventTypeRaw) {
return {
notFound: true,
} as const;
}
if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
requiresLoginToUpdate = true;
}
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
// @NOTE: had to do this because Server side cant return [Object objects]
// probably fixable with json.stringify -> json.parse
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
eventTypeRaw.users = !!eventTypeRaw.hosts?.length
? eventTypeRaw.hosts.map((host) => host.user)
: eventTypeRaw.users;
if (!eventTypeRaw.users.length) {
if (!eventTypeRaw.owner)
return {
notFound: true,
} as const;
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
}
const eventType = {
...eventTypeRaw,
periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
slug: eventType.team?.slug || eventType.users[0]?.username || null,
};
if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
await handleSeatsEventTypeOnBooking(eventType, bookingInfo, seatReferenceUid, session?.user.id);
}
const payment = await prisma.payment.findFirst({
where: {
bookingId: bookingInfo.id,
},
select: {
success: true,
refunded: true,
currency: true,
amount: true,
paymentOption: true,
},
});
return {
props: {
themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
profile,
eventType,
recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
trpcState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
paymentStatus: payment,
...(tz && { tz }),
userTimeFormat,
requiresLoginToUpdate,
},
};
}
async function getRecurringBookings(recurringEventId: string | null) {
if (!recurringEventId) return null;
const recurringBookings = await prisma.booking.findMany({
where: {
recurringEventId,
status: BookingStatus.ACCEPTED,
},
select: {
startTime: true,
},
});
return recurringBookings.map((obj) => obj.startTime.toString());
}

View File

@ -1,6 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
"use client";
import { getServerSideProps as _getServerSideProps } from "../[uid]";
import { getServerSideProps as _getServerSideProps } from "@lib/booking/[uid]/getServerSideProps";
import withEmbedSsr from "@lib/withEmbedSsr";
export { default } from "../[uid]";

View File

@ -1,74 +1,17 @@
import { getLayout } from "@calcom/features/MainLayout";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, ButtonGroup } from "@calcom/ui";
import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
"use client";
import { getLayout } from "@calcom/features/MainLayout";
import EnterprisePage from "@components/EnterprisePage";
import PageWrapper from "@components/PageWrapper";
export default function EnterprisePage() {
const { t } = useLocale();
const ProxifiedEnterprisePage = new Proxy<{
(): JSX.Element;
PageWrapper?: typeof PageWrapper;
getLayout?: typeof getLayout;
}>(EnterprisePage, {});
const features = [
{
icon: <Globe className="h-5 w-5 text-red-500" />,
title: t("branded_subdomain"),
description: t("branded_subdomain_description"),
},
{
icon: <BarChart className="h-5 w-5 text-blue-500" />,
title: t("org_insights"),
description: t("org_insights_description"),
},
{
icon: <Paintbrush className="h-5 w-5 text-pink-500" />,
title: t("extensive_whitelabeling"),
description: t("extensive_whitelabeling_description"),
},
{
icon: <Users className="h-5 w-5 text-orange-500" />,
title: t("unlimited_teams"),
description: t("unlimited_teams_description"),
},
{
icon: <CreditCard className="h-5 w-5 text-green-500" />,
title: t("unified_billing"),
description: t("unified_billing_description"),
},
{
icon: <Lock className="h-5 w-5 text-purple-500" />,
title: t("advanced_managed_events"),
description: t("advanced_managed_events_description"),
},
];
return (
<div>
<ShellMain heading="Enterprise" subtitle={t("enterprise_description")}>
<UpgradeTip
plan="enterprise"
title={t("create_your_org")}
description={t("create_your_org_description")}
features={features}
background="/tips/enterprise"
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href="https://i.cal.com/sales/enterprise?duration=25" target="_blank">
{t("contact_sales")}
</Button>
<Button color="minimal" href="https://cal.com/enterprise" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<>Create Org</>
</UpgradeTip>
</ShellMain>
</div>
);
}
ProxifiedEnterprisePage.PageWrapper = PageWrapper;
ProxifiedEnterprisePage.getLayout = getLayout;
EnterprisePage.PageWrapper = PageWrapper;
EnterprisePage.getLayout = getLayout;
export default ProxifiedEnterprisePage;

View File

@ -1,3 +1,5 @@
"use client";
import { getLayout } from "@calcom/features/MainLayout";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import {

View File

@ -1,3 +1,5 @@
"use client";
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";

View File

@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import type { z } from "zod";
@ -98,10 +100,15 @@ const AppearanceView = ({
reset: resetBookerLayoutThemeReset,
} = bookerLayoutFormMethods;
const DEFAULT_BRAND_COLOURS = {
light: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
dark: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
};
const brandColorsFormMethods = useForm({
defaultValues: {
brandColor: user.brandColor || DEFAULT_LIGHT_BRAND_COLOR,
darkBrandColor: user.darkBrandColor || DEFAULT_DARK_BRAND_COLOR,
brandColor: DEFAULT_BRAND_COLOURS.light,
darkBrandColor: DEFAULT_BRAND_COLOURS.dark,
},
});
@ -231,12 +238,12 @@ const AppearanceView = ({
<Controller
name="brandColor"
control={brandColorsFormMethods.control}
defaultValue={user.brandColor}
defaultValue={DEFAULT_BRAND_COLOURS.light}
render={() => (
<div>
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={user.brandColor}
defaultValue={DEFAULT_BRAND_COLOURS.light}
resetDefaultValue={DEFAULT_LIGHT_BRAND_COLOR}
onChange={(value) => {
try {
@ -260,12 +267,12 @@ const AppearanceView = ({
<Controller
name="darkBrandColor"
control={brandColorsFormMethods.control}
defaultValue={user.darkBrandColor}
defaultValue={DEFAULT_BRAND_COLOURS.dark}
render={() => (
<div className="mt-6 sm:mt-0">
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={user.darkBrandColor}
defaultValue={DEFAULT_BRAND_COLOURS.dark}
resetDefaultValue={DEFAULT_DARK_BRAND_COLOR}
onChange={(value) => {
try {

View File

@ -1,3 +1,5 @@
"use client";
import { Fragment } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";

View File

@ -1,3 +1,5 @@
"use client";
import { useReducer } from "react";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";

View File

@ -1,3 +1,5 @@
"use client";
import { useSession } from "next-auth/react";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";

View File

@ -1,3 +1,5 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { signOut, useSession } from "next-auth/react";
import type { BaseSyntheticEvent } from "react";

View File

@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";

View File

@ -1,3 +1,5 @@
"use client";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";
import { useForm } from "react-hook-form";

View File

@ -1,3 +1,5 @@
"use client";
import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";

View File

@ -1352,7 +1352,9 @@
"event_name_info": "اسم نوع الحدث",
"event_date_info": "تاريخ الحدث",
"event_time_info": "وقت بدء الحدث",
"location_variable": "الموقع",
"location_info": "موقع الحدث",
"additional_notes_variable": "ملاحظات إضافية",
"additional_notes_info": "ملاحظات إضافية للحجز",
"attendee_name_info": "اسم الشخص صاحب الحجز",
"organizer_name_info": "اسم المنظم",
@ -1803,6 +1805,7 @@
"verification_code": "رمز التحقق",
"can_you_try_again": "هل يمكنك المحاولة مرة أخرى في وقت مختلف؟",
"verify": "التحقق",
"timezone_variable": "المنطقة الزمنية",
"timezone_info": "المنطقة الزمنية للشخص الذي يتلقى الحجز",
"event_end_time_variable": "وقت انتهاء الحدث",
"event_end_time_info": "وقت نهاية الحدث",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Název typu události",
"event_date_info": "Datum události",
"event_time_info": "Čas začátku události",
"location_variable": "Místo",
"location_info": "Místo události",
"additional_notes_variable": "Doplňující poznámky",
"additional_notes_info": "Další poznámky k rezervaci",
"attendee_name_info": "Jméno osoby provádějící rezervaci",
"organizer_name_info": "Jméno organizátora",
@ -1803,6 +1805,7 @@
"verification_code": "Ověřovací kód",
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
"verify": "Ověřit",
"timezone_variable": "Časová zóna",
"timezone_info": "Časové pásmo přijímající osoby",
"event_end_time_variable": "Čas ukončení události",
"event_end_time_info": "Čas ukončení události",

View File

@ -1175,7 +1175,9 @@
"event_name_info": "Navn på begivenhedstypen",
"event_date_info": "Dato for begivenheden",
"event_time_info": "Starttidspunkt for begivenheden",
"location_variable": "Placering",
"location_info": "Begivenhedens placering",
"additional_notes_variable": "Yderligere bemærkninger",
"additional_notes_info": "De yderligere noter til booking",
"attendee_name_info": "Navn på personen der booker",
"to": "Til",
@ -1529,6 +1531,7 @@
"this_will_be_the_placeholder": "Dette vil være pladsholderen",
"verification_code": "Bekræftelseskode",
"verify": "Bekræft",
"timezone_variable": "Tidszone",
"confirm_your_details": "Bekræft dine oplysninger",
"overlay_my_calendar": "Vis min kalender",
"need_help": "Brug for hjælp?"

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Name des Ereignistyps",
"event_date_info": "Das Datum der Veranstaltung",
"event_time_info": "Die Startzeit des Termins",
"location_variable": "Ort",
"location_info": "Der Ort des Events",
"additional_notes_variable": "Zusätzliche Notizen",
"additional_notes_info": "Die zusätzlichen Anmerkungen der Buchung",
"attendee_name_info": "Name der buchenden Person",
"organizer_name_info": "Name des Organisators",
@ -1803,6 +1805,7 @@
"verification_code": "Bestätigungscode",
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
"verify": "Bestätigen",
"timezone_variable": "Zeitzone",
"timezone_info": "Die Zeitzone der empfangenden Person",
"event_end_time_variable": "Endzeitpunkt des Termins",
"event_end_time_info": "Der Endzeitpunkt des Termins",

View File

@ -275,5 +275,7 @@
"label": "Ετικέτα",
"edit": "Επεξεργασία",
"disable_guests": "Απενεργοποίηση επισκεπτών",
"location_variable": "Τοποθεσία",
"additional_notes_variable": "Πρόσθετες σημειώσεις",
"already_have_account": "Έχετε ήδη λογαριασμό;"
}

View File

@ -1387,7 +1387,9 @@
"event_date_info": "The event date",
"event_time_info": "The event start time",
"event_type_not_found": "EventType not Found",
"location_variable": "Location",
"location_info": "The location of the event",
"additional_notes_variable": "Additional notes",
"additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name",
"organizer_name_info": "Organizers name",
@ -1848,6 +1850,7 @@
"verification_code": "Verification code",
"can_you_try_again": "Can you try again with a different time?",
"verify": "Verify",
"timezone_variable": "Timezone",
"timezone_info": "The timezone of the person receiving",
"event_end_time_variable": "Event end time",
"event_end_time_info": "The event end time",
@ -2148,7 +2151,6 @@
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"join_event_location":"Join {{eventLocationType}}",
"join_meeting":"Join Meeting",
"troubleshooting":"Troubleshooting",
"calendars_were_checking_for_conflicts":"Calendars were checking for conflicts",
"availabilty_schedules":"Availability schedules",

View File

@ -1351,7 +1351,9 @@
"event_name_info": "Nombre del tipo de evento",
"event_date_info": "Fecha del evento",
"event_time_info": "Hora de inicio del evento",
"location_variable": "Lugar",
"location_info": "Ubicación del evento",
"additional_notes_variable": "Notas Adicionales",
"additional_notes_info": "Notas adicionales de la reserva",
"attendee_name_info": "Nombre de la persona que reserva",
"organizer_name_info": "Nombre del organizador",
@ -1802,6 +1804,7 @@
"verification_code": "Código de verificación",
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
"verify": "Verificar",
"timezone_variable": "Zona Horaria",
"timezone_info": "Zona horaria de la persona que recibe",
"event_end_time_variable": "Hora de finalización del evento",
"event_end_time_info": "La hora de finalización del evento",

View File

@ -697,7 +697,9 @@
"event_name_info": "Gertaeraren motaren izena",
"event_date_info": "Gertaeraren data",
"event_time_info": "Gertaeraren hasiera-ordua",
"location_variable": "Kokapena",
"location_info": "Gertaeraren kokapena",
"additional_notes_variable": "Ohar gehigarriak",
"additional_notes_info": "Erreserbaren ohar gehigarriak",
"organizer_name_info": "Antolatzailearen izena",
"download_responses": "Deskargatu erantzunak",
@ -757,5 +759,6 @@
"options": "Aukerak",
"add_an_option": "Gehitu aukera bat",
"radio": "Irratia",
"all_bookings_filter_label": "Erreserba guztiak"
"all_bookings_filter_label": "Erreserba guztiak",
"timezone_variable": "Ordu-eremua"
}

View File

@ -1358,7 +1358,9 @@
"event_name_info": "Nom du type d'événement",
"event_date_info": "Date de l'événement",
"event_time_info": "Heure de début de l'événement",
"location_variable": "Lieu",
"location_info": "Lieu de l'événement",
"additional_notes_variable": "Notes supplémentaires",
"additional_notes_info": "Notes supplémentaires de la réservation",
"attendee_name_info": "Nom du participant",
"organizer_name_info": "Nom de l'organisateur",
@ -1808,6 +1810,7 @@
"verification_code": "Code de vérification",
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
"verify": "Vérifier",
"timezone_variable": "Fuseau horaire",
"timezone_info": "Le fuseau horaire du destinataire",
"event_end_time_variable": "Heure de fin de l'événement",
"event_end_time_info": "L'heure de fin de l'événement",

View File

@ -1385,7 +1385,9 @@
"event_date_info": "תאריך האירוע",
"event_time_info": "שעת ההתחלה של האירוע",
"event_type_not_found": "EventType לא נמצא",
"location_variable": "מיקום",
"location_info": "מיקום האירוע",
"additional_notes_variable": "הערות נוספות",
"additional_notes_info": "הערות נוספות להזמנה",
"attendee_name_info": "שם האדם שביצע את ההזמנה",
"organizer_name_info": "שם המארגן/ת",
@ -1845,6 +1847,7 @@
"verification_code": "קוד אימות",
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
"verify": "אמת",
"timezone_variable": "אזור זמן",
"timezone_info": "אזור הזמן של האדם שיקבל את ההזמנה",
"event_end_time_variable": "שעת סיום האירוע",
"event_end_time_info": "שעת הסיום של האירוע",
@ -2191,6 +2194,7 @@
"success_entry_created": "רשומה חדשה נוצרה בהצלחה",
"booking_redirect_email_subject": "התראת הפניית הזמנה",
"booking_redirect_email_title": "התראת הפניית הזמנה",
"copy_link_booking_redirect_request": "העתקת קישור לבקשת שיתוף",
"booking_redirect_request_title": "בקשת הפניית הזמנות",
"select_team_member": "בחירת נציגות מהצוות",
"out_of_office_unavailable_list": "רשימת אי־זמינות למחוץ למשרד",

View File

@ -205,5 +205,8 @@
"minute_timeUnit": "Perc",
"remove_app": "Alkalmazás eltávolítása",
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
"web_conference": "Online konferencia"
"web_conference": "Online konferencia",
"location_variable": "Helyszín",
"additional_notes_variable": "Egyéb jegyzetek",
"timezone_variable": "Időzóna"
}

View File

@ -1351,7 +1351,9 @@
"event_name_info": "Nome del tipo di evento",
"event_date_info": "Data dell'evento",
"event_time_info": "Ora di inizio dell'evento",
"location_variable": "Luogo",
"location_info": "Luogo dell'evento",
"additional_notes_variable": "Note aggiuntive",
"additional_notes_info": "Note aggiuntive sulla prenotazione",
"attendee_name_info": "Nome del partecipante",
"organizer_name_info": "Nome organizzatore",
@ -1802,6 +1804,7 @@
"verification_code": "Codice di verifica",
"can_you_try_again": "Riprovare specificando un orario differente.",
"verify": "Verifica",
"timezone_variable": "Timezone",
"timezone_info": "Il fuso orario della persona che riceve la prenotazione",
"event_end_time_variable": "Ora di conclusione dell'evento",
"event_end_time_info": "Ora di conclusione dell'evento",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "イベントの種類の名前",
"event_date_info": "イベントの日付",
"event_time_info": "イベントの開始時刻",
"location_variable": "場所",
"location_info": "イベントの場所",
"additional_notes_variable": "備考",
"additional_notes_info": "予約の追加メモ",
"attendee_name_info": "予約者名",
"organizer_name_info": "主催者名",
@ -1803,6 +1805,7 @@
"verification_code": "確認コード",
"can_you_try_again": "別の時間帯でもう 1 度お試しください",
"verify": "確認する",
"timezone_variable": "タイムゾーン",
"timezone_info": "受信するユーザーのタイムゾーン",
"event_end_time_variable": "イベントの終了時刻",
"event_end_time_info": "イベントの終了時刻",

View File

@ -251,5 +251,6 @@
"invoices": "វិក្កយបត្រ",
"users": "អ្នកប្រើប្រាស់",
"user": "អ្នកប្រើប្រាស់",
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។"
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។",
"timezone_variable": "ល្វែងម៉ោង"
}

View File

@ -1352,7 +1352,9 @@
"event_name_info": "이벤트 유형 이름",
"event_date_info": "이벤트 날짜",
"event_time_info": "이벤트 시작 시간",
"location_variable": "위치",
"location_info": "이벤트 위치",
"additional_notes_variable": "추가 참고 사항",
"additional_notes_info": "예약 추가 메모",
"attendee_name_info": "예약자 이름",
"organizer_name_info": "주최자 이름",
@ -1803,6 +1805,7 @@
"verification_code": "인증 코드",
"can_you_try_again": "다른 시간으로 다시 해보시겠어요?",
"verify": "인증",
"timezone_variable": "시간대",
"timezone_info": "예약 수신자의 시간대",
"event_end_time_variable": "이벤트 종료 시간",
"event_end_time_info": "이벤트가 종료되는 시간",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Naam van het type gebeurtenis",
"event_date_info": "De datum van de gebeurtenis",
"event_time_info": "De begintijd van de gebeurtenis",
"location_variable": "Locatie",
"location_info": "De locatie van de gebeurtenis",
"additional_notes_variable": "Aanvullende opmerkingen",
"additional_notes_info": "De aanvullende opmerkingen bij de boeking",
"attendee_name_info": "De naam van de persoon die boekt",
"organizer_name_info": "Naam organisator",
@ -1803,6 +1805,7 @@
"verification_code": "Verificatiecode",
"can_you_try_again": "Kunt u het opnieuw proberen met een andere tijd?",
"verify": "Verifiëren",
"timezone_variable": "Tijdzone",
"timezone_info": "De tijdzone van de ontvanger",
"event_end_time_variable": "Eindtijd gebeurtenis",
"event_end_time_info": "De eindtijd van de gebeurtenis",

View File

@ -1157,7 +1157,9 @@
"event_name_info": "Navnet på hendelsestypen",
"event_date_info": "Dato for hendelse",
"event_time_info": "Start-tidspunkt for hendelsen",
"location_variable": "Sted",
"location_info": "Hendelsessted",
"additional_notes_variable": "Tilleggsinformasjon",
"additional_notes_info": "Tilleggsinformasjonen om bookingen",
"attendee_name_info": "Navnet på personen som booker",
"to": "Til",
@ -1385,5 +1387,6 @@
"configure": "Konfigurer",
"sso_configuration": "Enkel Pålogging",
"booking_confirmation_failed": "Booking-bekreftelse feilet",
"timezone_variable": "Tidssone",
"need_help": "Trenger du hjelp?"
}

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Nazwa typu wydarzenia",
"event_date_info": "Data wydarzenia",
"event_time_info": "Godzina rozpoczęcia wydarzenia",
"location_variable": "Lokalizacja",
"location_info": "Lokalizacja wydarzenia",
"additional_notes_variable": "Dodatkowe uwagi",
"additional_notes_info": "Dodatkowe uwagi dotyczące rezerwacji",
"attendee_name_info": "Imię i nazwisko osoby rezerwującej",
"organizer_name_info": "Nazwa organizatora",
@ -1803,6 +1805,7 @@
"verification_code": "Kod weryfikacyjny",
"can_you_try_again": "Czy możesz spróbować ponownie w innym terminie?",
"verify": "Zweryfikuj",
"timezone_variable": "Strefa Czasowa",
"timezone_info": "Strefa czasowa osoby rezerwowanej",
"event_end_time_variable": "Czas zakończenia wydarzenia",
"event_end_time_info": "Czas zakończenia wydarzenia",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "O nome do tipo de evento",
"event_date_info": "A data do evento",
"event_time_info": "O horário inicial do evento",
"location_variable": "Local",
"location_info": "O local do evento",
"additional_notes_variable": "Observações adicionais",
"additional_notes_info": "Notas adicionais da reserva",
"attendee_name_info": "O nome da pessoa que fez a reserva",
"organizer_name_info": "Nome do organizador",
@ -1803,6 +1805,7 @@
"verification_code": "Código de verificação",
"can_you_try_again": "Poderia tentar novamente em um horário diferente?",
"verify": "Verificar",
"timezone_variable": "Fuso Horário",
"timezone_info": "O fuso horário do destinatário",
"event_end_time_variable": "Horário de término do evento",
"event_end_time_info": "O horário de término do evento",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "O nome do tipo de evento",
"event_date_info": "A data do evento",
"event_time_info": "A hora de início do evento",
"location_variable": "Localização",
"location_info": "O local do evento",
"additional_notes_variable": "Notas Adicionais",
"additional_notes_info": "As notas adicionais da reserva",
"attendee_name_info": "O nome do responsável pela reserva",
"organizer_name_info": "Nome do organizador",
@ -1803,6 +1805,7 @@
"verification_code": "Código de verificação",
"can_you_try_again": "Pode tentar novamente noutra altura?",
"verify": "Verificar",
"timezone_variable": "Fuso Horário",
"timezone_info": "O fuso horário do anfitrião",
"event_end_time_variable": "Hora de fim do evento",
"event_end_time_info": "A hora de fim do evento",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Denumirea tipului de eveniment",
"event_date_info": "Data evenimentului",
"event_time_info": "Ora începerii evenimentului",
"location_variable": "Loc",
"location_info": "Locul de desfășurare a evenimentului",
"additional_notes_variable": "Note suplimentare",
"additional_notes_info": "Observații suplimentare privind rezervarea",
"attendee_name_info": "Numele persoanei care efectuează rezervarea",
"organizer_name_info": "Numele organizatorului",
@ -1803,6 +1805,7 @@
"verification_code": "Cod de verificare",
"can_you_try_again": "Puteți încerca din nou cu o oră diferită?",
"verify": "Verificare",
"timezone_variable": "Timezone",
"timezone_info": "Fusul orar al destinatarului",
"event_end_time_variable": "Oră de încheiere eveniment",
"event_end_time_info": "Ora de încheiere a evenimentului",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Название типа события",
"event_date_info": "Дата события",
"event_time_info": "Время начала события",
"location_variable": "Местоположение",
"location_info": "Место проведения события",
"additional_notes_variable": "Дополнительная информация",
"additional_notes_info": "Дополнительные заметки о бронировании",
"attendee_name_info": "Участник, на которого оформляется бронирование",
"organizer_name_info": "Имя организатора",
@ -1803,6 +1805,7 @@
"verification_code": "Код подтверждения",
"can_you_try_again": "Попробуйте указать другое время встречи.",
"verify": "Подтвердить",
"timezone_variable": "Часовой пояс",
"timezone_info": "Часовой пояс получателя",
"event_end_time_variable": "Время окончания события",
"event_end_time_info": "Время окончания события",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Ime tipa događaja",
"event_date_info": "Datum događaja",
"event_time_info": "Vreme početka događaja",
"location_variable": "Lokacija",
"location_info": "Lokacija događaja",
"additional_notes_variable": "Dodatne beleške",
"additional_notes_info": "Dodatne napomene o rezervaciji",
"attendee_name_info": "Ime osobe koja rezerviše",
"organizer_name_info": "Ime organizatora",
@ -1803,6 +1805,7 @@
"verification_code": "Verifikacioni kôd",
"can_you_try_again": "Možete li da pokušate sa drugim terminom?",
"verify": "Verifikuj",
"timezone_variable": "Vremenska zona",
"timezone_info": "Vremenska zona primaoca",
"event_end_time_variable": "Vreme završetka događaja",
"event_end_time_info": "Vreme završetka događaja",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Namn på händelsetyp",
"event_date_info": "Datum för händelsen",
"event_time_info": "Händelsens starttid",
"location_variable": "Plats",
"location_info": "Händelsens plats",
"additional_notes_variable": "Ytterligare inmatning",
"additional_notes_info": "Ytterligare bokningsanteckningar",
"attendee_name_info": "Personens bokningsnamn",
"organizer_name_info": "Arrangörens namn",
@ -1803,6 +1805,7 @@
"verification_code": "Verifieringskod",
"can_you_try_again": "Kan du försöka igen med en annan tid?",
"verify": "Verifiera",
"timezone_variable": "Tidszon",
"timezone_info": "Tidszon för mottagande person",
"event_end_time_variable": "Händelsens sluttid",
"event_end_time_info": "Händelsens sluttid",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Etkinlik türü adı",
"event_date_info": "Etkinlik tarihi",
"event_time_info": "Etkinlik başlama saati",
"location_variable": "Konum",
"location_info": "Etkinlik yeri",
"additional_notes_variable": "Ek notlar",
"additional_notes_info": "Ek rezervasyon notları",
"attendee_name_info": "Rezervasyon yaptıran kişinin adı",
"organizer_name_info": "Düzenleyenin adı",
@ -1803,6 +1805,7 @@
"verification_code": "Doğrulama kodu",
"can_you_try_again": "Başka bir saatle tekrar deneyebilir misiniz?",
"verify": "Doğrula",
"timezone_variable": "Saat dilimi",
"timezone_info": "Alıcının saat dilimi",
"event_end_time_variable": "Etkinlik bitiş saati",
"event_end_time_info": "Etkinlik bitiş saati",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "Назва типу заходу",
"event_date_info": "Дата заходу",
"event_time_info": "Час початку заходу",
"location_variable": "Розташування",
"location_info": "Місце проведення заходу",
"additional_notes_variable": "Додаткові примітки",
"additional_notes_info": "Додаткові примітки щодо бронювання",
"attendee_name_info": "Ім’я особи, яка бронює",
"organizer_name_info": "Ім’я організатора",
@ -1803,6 +1805,7 @@
"verification_code": "Код перевірки",
"can_you_try_again": "Виберіть інший час і повторіть спробу.",
"verify": "Перевірити",
"timezone_variable": "Часовий пояс",
"timezone_info": "Часовий пояс одержувача",
"event_end_time_variable": "Час завершення заходу",
"event_end_time_info": "Час завершення заходу",

View File

@ -1355,7 +1355,9 @@
"event_name_info": "Tên loại sự kiện",
"event_date_info": "Ngày sự kiện",
"event_time_info": "Thời gian bắt đầu sự kiện",
"location_variable": "Vị trí",
"location_info": "Địa điểm sự kiện",
"additional_notes_variable": "Ghi chú bổ sung",
"additional_notes_info": "Ghi chú thêm cho lịch hẹn",
"attendee_name_info": "Tên người tham gia lịch hẹn",
"organizer_name_info": "Tên của tổ chức",
@ -1807,6 +1809,7 @@
"verification_code": "Mã xác minh",
"can_you_try_again": "Bạn có thể thử lại lúc khác được chứ?",
"verify": "Xác minh",
"timezone_variable": "Múi giờ",
"timezone_info": "Múi giờ của người nhận",
"event_end_time_variable": "Thời gian kết thúc sự kiện",
"event_end_time_info": "Thời gian kết thúc sự kiện",

View File

@ -1353,7 +1353,9 @@
"event_name_info": "活动类型名称",
"event_date_info": "活动日期",
"event_time_info": "活动开始时间",
"location_variable": "位置",
"location_info": "活动位置",
"additional_notes_variable": "附加备注",
"additional_notes_info": "预约附加说明",
"attendee_name_info": "预约人的姓名",
"organizer_name_info": "组织者姓名",
@ -1804,6 +1806,7 @@
"verification_code": "验证码",
"can_you_try_again": "可否用不同的时间再次尝试?",
"verify": "验证",
"timezone_variable": "时区",
"timezone_info": "接收人的时区",
"event_end_time_variable": "活动结束时间",
"event_end_time_info": "活动结束时间",

View File

@ -1352,7 +1352,9 @@
"event_name_info": "活動類型名稱",
"event_date_info": "活動日期",
"event_time_info": "活動開始時間",
"location_variable": "地點",
"location_info": "活動地點",
"additional_notes_variable": "備註",
"additional_notes_info": "預約額外備註",
"attendee_name_info": "預約人姓名",
"organizer_name_info": "主辦者姓名",
@ -1803,6 +1805,7 @@
"verification_code": "驗證碼",
"can_you_try_again": "您可以再試試其他時間嗎?",
"verify": "驗證",
"timezone_variable": "時區",
"timezone_info": "接收人的時區",
"event_end_time_variable": "活動結束時間",
"event_end_time_info": "活動結束時間",

View File

@ -10,6 +10,7 @@ import dayjs from "@calcom/dayjs";
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
@ -144,8 +145,8 @@ const BookerComponent = ({
: bookerLayouts.defaultLayout;
useBrandColors({
brandColor: event.data?.profile.brandColor,
darkBrandColor: event.data?.profile.darkBrandColor,
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
darkBrandColor: event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
theme: event.data?.profile.theme,
});

View File

@ -26,7 +26,6 @@ const BrandColorsForm = ({
const brandColorsFormMethods = useFormContext();
const {
formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty },
handleSubmit,
} = brandColorsFormMethods;
const [isCustomBrandColorChecked, setIsCustomBrandColorChecked] = useState(

View File

@ -180,8 +180,8 @@ const OrgAppearanceView = ({
}}>
<BrandColorsForm
onSubmit={onBrandColorsFormSubmit}
brandColor={currentOrg?.brandColor}
darkBrandColor={currentOrg?.darkBrandColor}
brandColor={currentOrg?.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR}
darkBrandColor={currentOrg?.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR}
/>
</Form>

View File

@ -1,3 +1,5 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

View File

@ -61,7 +61,10 @@ const ProfileView = ({ team }: ProfileViewProps) => {
await utils.viewer.teams.get.invalidate();
if (res) {
resetTheme({ theme: res.theme });
resetBrandColors({ brandColor: res.brandColor, darkBrandColor: res.darkBrandColor });
resetBrandColors({
brandColor: res.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
darkBrandColor: res.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
});
}
showToast(t("your_team_updated_successfully"), "success");
@ -139,8 +142,8 @@ const ProfileView = ({ team }: ProfileViewProps) => {
}}>
<BrandColorsForm
onSubmit={onBrandColorsFormSubmit}
brandColor={team?.brandColor}
darkBrandColor={team?.darkBrandColor}
brandColor={team?.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR}
darkBrandColor={team?.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR}
/>
</Form>

View File

@ -3,7 +3,8 @@ import { createHmac } from "crypto";
import { compile } from "handlebars";
import { getHumanReadableLocationValue } from "@calcom/app-store/locations";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { getUTCOffsetByTimezone } from "@calcom/lib/date-fns";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
type ContentType = "application/json" | "application/x-www-form-urlencoded";
@ -16,6 +17,18 @@ export type EventTypeInfo = {
length?: number | null;
};
export type UTCOffset = {
utcOffset?: number | null;
};
export type WithUTCOffsetType<T> = T & {
user?: Person & UTCOffset;
} & {
organizer?: Person & UTCOffset;
} & {
attendees?: (Person & UTCOffset)[];
};
export type WebhookDataType = CalendarEvent &
EventTypeInfo & {
metadata?: { [key: string]: string | number | boolean | null };
@ -32,14 +45,34 @@ export type WebhookDataType = CalendarEvent &
paymentId?: number;
};
function addUTCOffset(
data: Omit<WebhookDataType, "createdAt" | "triggerEvent">
): WithUTCOffsetType<WebhookDataType> {
if (data.organizer?.timeZone) {
(data.organizer as Person & UTCOffset).utcOffset = getUTCOffsetByTimezone(
data.organizer.timeZone,
data.startTime
);
}
if (data?.attendees?.length) {
(data.attendees as (Person & UTCOffset)[]).forEach((attendee) => {
attendee.utcOffset = getUTCOffsetByTimezone(attendee.timeZone, data.startTime);
});
}
return data as WithUTCOffsetType<WebhookDataType>;
}
function getZapierPayload(
data: CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }
data: WithUTCOffsetType<CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }>
): string {
const attendees = data.attendees.map((attendee) => {
const attendees = (data.attendees as (Person & UTCOffset)[]).map((attendee) => {
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
utcOffset: attendee.utcOffset,
};
});
@ -62,6 +95,7 @@ function getZapierPayload(
name: data.organizer.name,
email: data.organizer.email,
timeZone: data.organizer.timeZone,
utcOffset: data.organizer.utcOffset,
locale: data.organizer.locale,
},
eventType: {
@ -109,6 +143,8 @@ const sendPayload = async (
!template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded";
data.description = data.description || data.additionalNotes;
data = addUTCOffset(data);
let body;
/* Zapier id is hardcoded in the DB, we send the raw data for this case */

View File

@ -226,3 +226,15 @@ export const isInDST = (date: Dayjs) => {
return timeZoneWithDST(timeZone) && date.utcOffset() === getUTCOffsetInDST(timeZone);
};
/**
* Get UTC offset of given time zone
* @param timeZone Time Zone Name (Ex. America/Mazatlan)
* @param date
* @returns
*/
export function getUTCOffsetByTimezone(timeZone: string, date?: string | Date | Dayjs) {
if (!timeZone) return null;
return dayjs(date).tz(timeZone).utcOffset();
}

View File

@ -1,7 +1,9 @@
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
const BRAND_COLOR = "#292929";
const DARK_BRAND_COLOR = "#fafafa";
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "./constants";
const BRAND_COLOR = DEFAULT_LIGHT_BRAND_COLOR;
const DARK_BRAND_COLOR = DEFAULT_DARK_BRAND_COLOR;
type Rgb = {
r: number;

View File

@ -0,0 +1,17 @@
ALTER TABLE "Team" ALTER COLUMN "brandColor" DROP NOT NULL,
ALTER COLUMN "brandColor" DROP DEFAULT,
ALTER COLUMN "darkBrandColor" DROP NOT NULL,
ALTER COLUMN "darkBrandColor" DROP DEFAULT;
-- AlterTable
ALTER TABLE "users" ALTER COLUMN "brandColor" DROP NOT NULL,
ALTER COLUMN "brandColor" DROP DEFAULT,
ALTER COLUMN "darkBrandColor" DROP NOT NULL,
ALTER COLUMN "darkBrandColor" DROP DEFAULT;
UPDATE "Team" SET "brandColor" = NULL WHERE "brandColor" = '#292929';
UPDATE "Team" SET "darkBrandColor" = NULL WHERE "darkBrandColor" = '#fafafa';
UPDATE "users" SET "brandColor" = NULL WHERE "brandColor" = '#292929';
UPDATE "users" SET "darkBrandColor" = NULL WHERE "darkBrandColor" = '#fafafa';

View File

@ -224,8 +224,8 @@ model User {
availability Availability[]
invitedTo Int?
webhooks Webhook[]
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
brandColor String?
darkBrandColor String?
// the location where the events will end up
destinationCalendar DestinationCalendar?
away Boolean @default(false)
@ -299,8 +299,8 @@ model Team {
/// @zod.custom(imports.teamMetadataSchema)
metadata Json?
theme String?
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
brandColor String?
darkBrandColor String?
verifiedNumbers VerifiedNumber[]
parentId Int?
parent Team? @relation("organization", fields: [parentId], references: [id], onDelete: Cascade)

View File

@ -46,7 +46,7 @@ export function TopBanner(props: TopBannerProps) {
"flex w-full items-start justify-between gap-8 px-4 py-2 text-center lg:items-center",
variantClassName[variant]
)}>
<div className="flex flex-1 flex-col items-start justify-center gap-2 p-1 lg:flex-row lg:items-center">
<div className="flex flex-1 flex-col items-start justify-center gap-2 px-1 py-0.5 lg:flex-row lg:items-center">
<p className="text-emphasis flex flex-col items-start justify-center gap-2 text-left font-sans text-sm font-medium leading-4 lg:flex-row lg:items-center">
{Icon ? <Icon data-testid="variant-default" {...defaultIconProps} /> : defaultIcon}
{text}

View File

@ -26,7 +26,7 @@
"dependencies": {
"@calcom/lib": "*",
"@calcom/trpc": "*",
"@formkit/auto-animate": "^0.8.1",
"@formkit/auto-animate": "1.0.0-beta.5",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-popover": "^1.0.2",