Compare commits

...
20 Commits
Author SHA1 Message Date
Keith Williams 828ef8580e v3.0.5
release.yml / release (push) Canceled after 0s
2023-06-21 18:39:37 +02:00
Jeroen Reumkens a348c3a20a CAL-1892: Show booker layout dark mode preview based on theme the user selects (#9664) 2023-06-21 17:14:23 +02:00
Omar López 3c68c7f7cb chore: Upgrades prisma to 4.16 (#9670)
* Upgrades prisma to 4.16

* typefix
2023-06-21 17:07:54 +02:00
Hariom Balhara 820b2da16d test: Add RegExp tests for Next.config.js rewrites (#9681)
* Add RegExp tests

* Update apps/web/test/lib/next-config.test.ts

* Dont avoid rewrite for /booked event slug

* Add /embed/embed.js test

* Add dynamic group booking link test
2023-06-21 17:06:44 +02:00
sean-brydon 6bdb2b94eb feat: Adds sheet component (#9680)
* Adds sheet component

* Fix overlay

* Calc math
2023-06-21 15:29:23 +01:00
Hariom Balhara 3c3db0b3a6 fix: Hotfix/ Dynamic Group Booking Links not working in new booker (#9685)
* Debuglogs

* Derive group usernames correctly

* Revert "Debuglogs"

This reverts commit d93f031589b34bb7fee32a34f96eb89805c20275.
2023-06-21 15:22:13 +01:00
Hariom Balhara 4748641c22 fix: Hotfix/main - 404 workflows (#9678) 2023-06-21 15:39:03 +05:30
Jeroen Reumkens 804787f2a3 Get brand colors from DB for owner, so it's reflected in booker (#9679) 2023-06-21 09:04:15 +00:00
Alex van Andel b7c3b31e0c fix: eventType-getByViewer subselect on children->users[] (#9668) 2023-06-21 08:56:42 +00:00
Jeroen ReumkensandHariom Balhara ab6781cd72 fix: Booker fix for team + user with same event + event slug (#9654)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-06-21 13:01:55 +05:30
Crowdin Bot b070cfb227 New Crowdin translations by Github Action 2023-06-21 07:29:17 +00:00
Hariom Balhara 5acb3a09e5 fix: Hide EventTypeDetails when configured (#9633) 2023-06-21 12:58:21 +05:30
Crowdin Bot 69c9f690cb New Crowdin translations by Github Action 2023-06-20 17:55:04 +00:00
Ayush Mainali d2a12ea834 refactor: Convert card to cva (#9651) 2023-06-20 19:54:08 +02:00
16f24b4831 feat: [CAL-1435] Emptyscreen for App Store if no external app has been configured (CALCOM-8154) (#9516)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-06-20 19:53:33 +02:00
sean-brydon a3eeeab259 feat: Handle deleting vercel domain when org is deleted (#9661) 2023-06-20 17:57:21 +02:00
Alex van Andel f69c254fbc v3.0.4
release.yml / release (push) Canceled after 0s
2023-06-20 17:37:53 +02:00
Hariom Balhara f0f6bd4846 fix: Team links and username starting with team rewrites (#9665)
* Fix

* Fix private link /book page as well
2023-06-20 16:48:15 +02:00
Udit TakkarandPeer Richelsen 8816a18854 feat: team filters (#9543)
* feat: team filers

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: type error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: remove filter from mobile

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: show heading when one item selected

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-06-20 16:39:18 +02:00
sean-brydon 4e529e5c6c test: Adds permission tests to impersonation (#9660)
* Adds permission tests to impersonation

* NIT + new test for none numbers
2023-06-20 13:42:37 +00:00
63 changed files with 1383 additions and 406 deletions

No files matched your search

@@ -2,7 +2,7 @@ import { CAL_URL } from "@calcom/lib/constants";
export default function getCalLink(eventType: { export default function getCalLink(eventType: {
team?: { slug: string | null } | null; team?: { slug: string | null } | null;
owner: { username: string | null } | null; owner?: { username: string | null } | null;
users?: { username: string | null }[]; users?: { username: string | null }[];
slug: string; slug: string;
}) { }) {
@@ -28,8 +28,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
{emptyStore ? ( {emptyStore ? (
<EmptyScreen <EmptyScreen
Icon={AlertCircle} Icon={AlertCircle}
headline={t("no_apps")} headline={isAdmin ? t("no_apps") : t("no_apps_configured")}
description={isAdmin ? "You can enable apps in the settings" : ""} description={isAdmin ? t("enable_in_settings") : t("please_contact_admin")}
buttonText={isAdmin ? t("apps_settings") : ""} buttonText={isAdmin ? t("apps_settings") : ""}
buttonOnClick={() => router.push("/settings/admin/apps/calendar")} buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
/> />
@@ -38,6 +38,7 @@ const generateHashedLink = (id: number) => {
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => { export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery(); const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
const { data: user } = trpc.viewer.me.useQuery();
const formMethods = useFormContext<FormValues>(); const formMethods = useFormContext<FormValues>();
const { t } = useLocale(); const { t } = useLocale();
@@ -49,6 +50,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const bookingFields: Prisma.JsonObject = {}; const bookingFields: Prisma.JsonObject = {};
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow); const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
const selectedThemeIsDark =
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => { eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name + " input"; bookingFields[name] = name + " input";
@@ -162,7 +166,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</div> </div>
<hr className="border-subtle [&:has(+div:empty)]:hidden" /> <hr className="border-subtle [&:has(+div:empty)]:hidden" />
<div> <div>
<BookerLayoutSelector fallbackToUserSettings /> <BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} />
</div> </div>
<hr className="border-subtle" /> <hr className="border-subtle" />
<FormBuilder <FormBuilder
+20 -23
View File
@@ -1,10 +1,10 @@
require("dotenv").config({ path: "../../.env" }); require("dotenv").config({ path: "../../.env" });
const CopyWebpackPlugin = require("copy-webpack-plugin"); const CopyWebpackPlugin = require("copy-webpack-plugin");
const os = require("os"); const os = require("os");
const glob = require("glob");
const englishTranslation = require("./public/static/locales/en/common.json"); const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom"); const { withAxiom } = require("next-axiom");
const { i18n } = require("./next-i18next.config"); const { i18n } = require("./next-i18next.config");
const { pages } = require("./pages");
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET"); if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY"); if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
@@ -82,16 +82,19 @@ if (process.env.ANALYZE === "true") {
plugins.push(withAxiom); plugins.push(withAxiom);
/** Needed to rewrite public booking page, gets all static pages but [user] */ // .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
const pages = glob // It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname }) // It would also not match /free/30min/embed because we are ensuring just two slashes
.map((filename) => // ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page.
filename // [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
.substr(6) // book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
.replace(/(\.tsx|\.js|\.ts)/, "")
.replace(/\/.*/, "") // Important Note: When modifying these RegExps update apps/web/test/lib/next-config.test.ts as well
) const userTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type((?!book$)[^/]+)`;
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]")); const teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
const privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
const embedUserTypeRouteRegExp = `/:user((?!${pages.join("|")}).*)/:type/embed`;
const embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const nextConfig = { const nextConfig = {
@@ -183,12 +186,6 @@ const nextConfig = {
return config; return config;
}, },
async rewrites() { async rewrites() {
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
// It would also not match /free/30min/embed because we are ensuring just two slashes
// ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page.
// [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
const userTypeRouteRegExp = `/:user((?!${pages.join("|")})[^/]*)/:type((?!book)[^/]+)`;
let rewrites = [ let rewrites = [
{ {
source: "/org/:slug", source: "/org/:slug",
@@ -233,12 +230,12 @@ const nextConfig = {
has: [{ type: "cookie", key: "new-booker-enabled" }], has: [{ type: "cookie", key: "new-booker-enabled" }],
}, },
{ {
source: "/team/:slug/:type", source: teamTypeRouteRegExp,
destination: "/new-booker/team/:slug/:type", destination: "/new-booker/team/:slug/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }], has: [{ type: "cookie", key: "new-booker-enabled" }],
}, },
{ {
source: "/d/:link/:slug", source: privateLinkRouteRegExp,
destination: "/new-booker/d/:link/:slug", destination: "/new-booker/d/:link/:slug",
has: [{ type: "cookie", key: "new-booker-enabled" }], has: [{ type: "cookie", key: "new-booker-enabled" }],
}, },
@@ -246,12 +243,12 @@ const nextConfig = {
// Keep cookie based booker enabled to test new-booker embed in production // Keep cookie based booker enabled to test new-booker embed in production
...[ ...[
{ {
source: `/:user((?!${pages.join("|")}).*)/:type/embed`, source: embedUserTypeRouteRegExp,
destination: "/new-booker/:user/:type/embed", destination: "/new-booker/:user/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }], has: [{ type: "cookie", key: "new-booker-enabled" }],
}, },
{ {
source: "/team/:slug/:type/embed", source: embedTeamTypeRouteRegExp,
destination: "/new-booker/team/:slug/:type/embed", destination: "/new-booker/team/:slug/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }], has: [{ type: "cookie", key: "new-booker-enabled" }],
}, },
@@ -293,11 +290,11 @@ const nextConfig = {
destination: "/new-booker/:user/:type", destination: "/new-booker/:user/:type",
}, },
{ {
source: "/team/:slug/:type", source: teamTypeRouteRegExp,
destination: "/new-booker/team/:slug/:type", destination: "/new-booker/team/:slug/:type",
}, },
{ {
source: "/d/:link/:slug", source: privateLinkRouteRegExp,
destination: "/new-booker/d/:link/:slug", destination: "/new-booker/d/:link/:slug",
}, },
] ]
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "3.0.3", "version": "3.0.5",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
+14
View File
@@ -0,0 +1,14 @@
const glob = require("glob");
/** 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]"));
exports.pages = pages;
+55 -47
View File
@@ -13,7 +13,8 @@ import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components"; import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog"; import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog"; import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
import { OrganizationEventTypeFilter } from "@calcom/features/eventtypes/components/OrganizationEventTypeFilter"; import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -47,7 +48,6 @@ import {
HeadSeo, HeadSeo,
Skeleton, Skeleton,
Label, Label,
VerticalDivider,
Alert, Alert,
} from "@calcom/ui"; } from "@calcom/ui";
import { import {
@@ -420,7 +420,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
truncateAfter={4} truncateAfter={4}
items={type.children items={type.children
.flatMap((ch) => ch.users) .flatMap((ch) => ch.users)
.map((user: User) => ({ .map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "", alt: user.name || "",
image: `${WEBAPP_URL}/${user.username}/avatar.png`, image: `${WEBAPP_URL}/${user.username}/avatar.png`,
title: user.name || "", title: user.name || "",
@@ -800,8 +800,7 @@ const CTA = () => {
const Actions = () => { const Actions = () => {
return ( return (
<div className="hidden items-center md:flex"> <div className="hidden items-center md:flex">
<OrganizationEventTypeFilter /> <TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
<VerticalDivider />
</div> </div>
); );
}; };
@@ -834,9 +833,6 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
); );
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any);
const EventTypesPage = () => { const EventTypesPage = () => {
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
@@ -862,6 +858,10 @@ const EventTypesPage = () => {
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const filters = getTeamsFiltersFromQuery(router.query);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any, { filters });
return ( return (
<div> <div>
@@ -879,47 +879,55 @@ const EventTypesPage = () => {
CTA={<CTA />}> CTA={<CTA />}>
<WithQuery <WithQuery
customLoader={<SkeletonLoader />} customLoader={<SkeletonLoader />}
success={({ data }) => ( success={({ data }) => {
<> const isFilteredByOnlyOneItem =
{data.eventTypeGroups.length > 1 ? ( (filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
<> data.eventTypeGroups.length === 1;
{isMobile ? (
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
orgSlug={orgBranding?.slug}
/>
<EventTypeList return (
types={group.eventTypes} <>
group={group} {data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
groupIndex={index} <>
readOnly={group.metadata.readOnly} {isMobile ? (
/> <MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
</div> ) : (
)) data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
)} <div className="flex flex-col" key={group.profile.slug}>
</> <EventTypeListHeading
) : data.eventTypeGroups.length === 1 ? ( profile={group.profile}
<EventTypeList membershipCount={group.metadata.membershipCount}
types={data.eventTypeGroups[0].eventTypes} teamId={group.teamId}
group={data.eventTypeGroups[0]} orgSlug={orgBranding?.slug}
groupIndex={0} />
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
) : (
<CreateFirstEventTypeView />
)}
<EmbedDialog /> <EventTypeList
{router.query.dialog === "duplicate" && <DuplicateDialog />} types={group.eventTypes}
</> group={group}
)} groupIndex={index}
readOnly={group.metadata.readOnly}
/>
</div>
))
)}
</>
) : (
data.eventTypeGroups.length === 1 && (
<EventTypeList
types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]}
groupIndex={0}
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
)
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
{router.query.dialog === "duplicate" && <DuplicateDialog />}
</>
);
}}
/> />
</Shell> </Shell>
</div> </div>
+13 -2
View File
@@ -14,7 +14,7 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>; type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) { export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent }: PageProps) {
return ( return (
<main className="flex h-full min-h-[100dvh] items-center justify-center"> <main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo <BookerSeo
@@ -29,6 +29,7 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
rescheduleBooking={booking} rescheduleBooking={booking}
isAway={away} isAway={away}
hideBranding={isBrandingHidden} hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
/> />
</main> </main>
); );
@@ -57,6 +58,11 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
username: true, username: true,
}, },
}, },
team: {
select: {
id: true,
},
},
}, },
}, },
}, },
@@ -96,9 +102,11 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`); booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
} }
const isTeamEvent = !!hashedLink.eventType?.team?.id;
// We use this to both prefetch the query on the server, // We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise. // as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug }); const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent });
if (!eventData) { if (!eventData) {
return { return {
@@ -114,6 +122,9 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
slug, slug,
trpcState: ssr.dehydrate(), trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding, isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
}, },
}; };
} }
@@ -30,6 +30,7 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
rescheduleBooking={booking} rescheduleBooking={booking}
isAway={away} isAway={away}
hideBranding={isBrandingHidden} hideBranding={isBrandingHidden}
isTeamEvent
/> />
</main> </main>
); );
@@ -71,7 +72,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
// We use this to both prefetch the query on the server, // We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise. // as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username: teamSlug, eventSlug: meetingSlug }); const eventData = await ssr.viewer.public.event.fetch({
username: teamSlug,
eventSlug: meetingSlug,
isTeamEvent: true,
});
if (!eventData) { if (!eventData) {
return { return {
@@ -70,6 +70,13 @@ const AppearanceView = () => {
}, },
}); });
const selectedTheme = formMethods.watch("theme");
const selectedThemeIsDark =
selectedTheme === "dark" ||
(selectedTheme === "" &&
typeof document !== "undefined" &&
document.documentElement.classList.contains("dark"));
const { const {
formState: { isSubmitting, isDirty }, formState: { isSubmitting, isDirty },
reset, reset,
@@ -144,6 +151,7 @@ const AppearanceView = () => {
<hr className="border-subtle my-8 border [&:has(+hr)]:hidden" /> <hr className="border-subtle my-8 border [&:has(+hr)]:hidden" />
<BookerLayoutSelector <BookerLayoutSelector
isDark={selectedThemeIsDark}
name="metadata.defaultBookerLayouts" name="metadata.defaultBookerLayouts"
title={t("bookerlayout_user_settings_title")} title={t("bookerlayout_user_settings_title")}
description={t("bookerlayout_user_settings_description")} description={t("bookerlayout_user_settings_description")}
@@ -0,0 +1,78 @@
<svg width="208" height="120" viewBox="0 0 208 120" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_5291_194335)">
<rect width="208" height="120" rx="6" fill="url(#paint0_radial_5291_194335)"/>
<rect x="25.4497" y="14.6206" width="157.101" height="90.7591" rx="5.35267" fill="#1A1A1A"/>
<circle cx="35.5909" cy="24.7618" r="2.67634" fill="#2B2B2B"/>
<rect x="40.0518" y="23.8696" width="14.2738" height="1.78422" rx="0.892112" fill="#2B2B2B"/>
<rect x="32.9146" y="31.0068" width="28.5476" height="2.67634" rx="0.892112" fill="#444444"/>
<rect x="32.9146" y="37.2515" width="21.4107" height="1.78422" rx="0.892112" fill="#2B2B2B"/>
<rect x="32.9146" y="42.604" width="21.4107" height="1.78422" rx="0.892112" fill="#2B2B2B"/>
<rect x="32.9146" y="47.957" width="21.4107" height="1.78422" rx="0.892112" fill="#2B2B2B"/>
<rect x="32.9146" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="40.0518" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="47.1885" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="54.3252" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="61.4619" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="68.5991" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="75.7363" y="58.6621" width="5.35267" height="1.78422" rx="0.892112" fill="#444444"/>
<rect x="47.1885" y="64.0146" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="54.3252" y="64.0146" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="61.4619" y="64.0146" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="68.5991" y="64.0146" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="75.7363" y="64.0146" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="32.9146" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="40.0518" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="47.1885" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="54.3252" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="61.4619" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="68.5991" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="75.7363" y="72.936" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="32.9146" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="40.0518" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="47.1885" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="54.3252" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="61.4619" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#575757"/>
<rect x="68.5991" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="75.7363" y="81.8569" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="32.9146" y="90.7783" width="5.35267" height="5.35267" rx="0.892112" fill="#1C1C1C"/>
<rect x="40.0518" y="90.7783" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="47.1885" y="90.7783" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="54.3252" y="90.7783" width="5.35267" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<path d="M89.1177 18.5171V101.484H90.9019V18.5171H89.1177Z" fill="#1C1C1C" mask="url(#path-3-inside-1_5291_194335)"/>
<rect x="96.5796" y="22.0854" width="21.4107" height="3.56845" rx="0.892112" fill="#444444"/>
<rect x="96.5796" y="29.2227" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="96.5796" y="38.1436" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="96.5796" y="47.0645" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="96.5796" y="55.9858" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="96.5796" y="64.9067" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="96.5796" y="73.8281" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="22.0854" width="21.4107" height="3.56845" rx="0.892112" fill="#444444"/>
<rect x="125.127" y="29.2227" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="38.1436" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="47.0649" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="55.9858" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="64.9067" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="125.127" y="73.8281" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="22.0854" width="21.4107" height="3.56845" rx="0.892112" fill="#444444"/>
<rect x="153.675" y="29.2227" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="38.1436" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="47.0645" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="55.9858" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="64.9067" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect x="153.675" y="73.8281" width="21.4107" height="5.35267" rx="0.892112" fill="#2B2B2B"/>
<rect opacity="0.2" x="0.666992" y="-1" width="208" height="120" fill="url(#pattern0)"/>
</g>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_5291_194335" transform="scale(0.00480769 0.00833333)"/>
</pattern>
<radialGradient id="paint0_radial_5291_194335" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(207.334 -1.47576e-05) rotate(145.398) scale(465.773 828.954)">
<stop stop-color="#282E39"/>
<stop offset="1" stop-color="#D4D4D5"/>
</radialGradient>
<clipPath id="clip0_5291_194335">
<rect width="208" height="120" rx="6" fill="white"/>
</clipPath>
<image id="image0_5291_194335" width="208" height="120" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANAAAAB4CAMAAAC0NYzFAAAAWlBMVEX////4+Pj4+Pj4+Pjw8PDw8PDo6Ojg4ODY2NjY2NjQ0NDIyMjAwMC4uLiwsLC4uLjIyMjIyMjU1NTY2Njc3Nzg4ODg4ODk5OTo6Ojo6Ojs7Ozs7Ozu7u709PT32s2WAAAAHnRSTlNISExKTFBQUFhQWGBgYGhoXFhYVFRUUlBPTk5QTEx67T/AAAAjPElEQVR4AUX6R6IjTW8Gaj7giXgRiUyymvr0yx3V/pfZ7jpM4Ifw4PdYhB95kiuLlTNl3v5s7wsXR1OL5IN+ckY4RfsefToFnPn9RXOVE2e6gOs9qsGjAPx4cbB+7HOGZXjx9KuzB/jBCdfjDVpD5mP4I9TvcLpCRvPKnwPOE9lAZ/qiOj0Ab5wrWGRTnEnqViknwYTAdiL41PXp8Mmet1ztAYHF5SQBXFWrhyhWuNPQ17Xz7tUc0OZxnEMd8OySy41KWN3Bt7C8d+GE9OVnZarLHiPiOjPqjpRInGLGtS9w0m/iRVt+f53kKo+KuszceTEbOeoJ+fPU/0G52ulaqxYSeiY9aN9db+MrCjScCvDvZGIa5GscCLwQp+DTme1pqHwcuX6ndBMsdwD22b6mrg8HL41r55nNvelE4LTR2So2PX0T3PkKLxjom9ly4PRjrbQThxosIv8+bJkOioucXui/TbOvryX13ZjMk6XR/G6LOqWuPm9h9jkGcbEdoGkmHO/LWt/LZ3pPsrztuUU+Bmt7LcDlPR9PXCoLLv+Pts873Dk4W08/Gm6wnBOMi8hfpvmU6Zy3zKFY6aXXkpepqWstHlc0CVcJtbsKhvZ0wKnRgyxPu2L9yAmmvHD58w68UmRvRj+lyy1gNWgsxbbHnT7H/vvXcFqHmu0hcXPz7ldGRl2P5YgHecGRLj5Zbk4rssvLQffjZb+ZzSt0y+HK08Ei4IXrJNWWqdOX7j/93/997c8RXdsBuAZkGpIFxkKmvk7dFF44ZI5ebJ7s/qdr6iJ0H3KFt4XWzTEHvHveF19HymI/m7Nh6YO2oi2db4gvVCypxqqeOnsE/tuYpr1+fuCJyhDpke/wsZiOdYHIja9X9+NUiHNvX/B+2oGTPsP9ULMpYUkTzt+/UKnMbq2Bws5y5wMRy3VBdA/kRizDHejVdStjp02n0oC+pQx6vriyUbcR2oVDL0BaHsJzedjFp/a1H7iOGbdw3YN98bb4SLm+HFvGj8HU3eqgPpUYz6CKXgXSdR4NA1cExPBMpItzGbnDRprbcYGM3mir5l8DlmUtPwiHBWsZ41H78fBtwNTtqNQXai1Oujk7X5Z3Oc8nTtA5+hC9mE2WkZ7ZQzM3C4GimVpQGccPJnN5OOwCZ2+VlcGjTgvjDAzJbSZ5Xy6v/VIsxiC2BQuvvUcn8rRTfbTODQTXnSEvJef3d0/ZXrWsjF7qZXcMU2G/K8LPcp3+PvvpN0YLgG23vohhpd1iep/Co/vBD5OmHWqK6gqpP1wugJjg3uIH2ftNrudMjavqlQcGBNvD7y9CL0hoK/vAnM/ZvISdxwxiw2Tj9NAsPd1q5s11Nte4Gy+FFGElaPuuTI4LOpwNGTuWYz/z7hUCYR53MQBi9ukJ2kighxpNh0yZ9kulLdLUfbzy9YprR2vaORnC7FtrLTJ7RuR//ufWPbvcDUwW7WM4Penibq5xJM6nsqc4t1fg2w0uhKMXIeFTB3nrO4M3VgvwFuj27L/7mF+jWM2cJ+ivWE23x1xblXH27c3bKHj10eeKF8rsSMXDkha2LTrmPLnC3qTNfidaHYk9NNSmndBPBs5MYB4/Cvxwdp2eR3rMhhf2s191nAQmTaf2wz6a1ZxUzmTesHJ74OrJQ3qf6+pPH+i4ppfv7Kg9/3LITIgoZ628cLZV51hLvXeVc/aCnj/nyPhSu5m9zLesTJ9aHd7bz+dpncXdBZl+OHsEPftcnMhXBtiDi5OiOx5PH/aj7nOabA0bAUbrvqvfqZuWxFyyb21YpqQGt/3hXWE2d/4PpjjmtPZSHIgVtjp9A9VY3nt5ZhrGbljy2FYRDTiOrgsJfX1suevN+PQ/0/SHOWkv1e86PqinnDL4wimo96bO769czOy4ihcv/Agw7q3p6TM1RecELpguR6gYhFdbVI3jBkaPB33VrTdX15IWg1Y31BuFR2UYQcUUdKhehohL5BSTYa/WuhHuHNLTfjztxbwBE4J36SOjy8K8I07GtcpqQNG9jxXYLnoJ9dnkj9GnBl5Z4NyfGtRT2jOFqa5hz+a11umHc1rqUPQgplsDn6tf+T9SGTQ97EdXYIN4x8OfATCO9ihw1zZ2Z0G+3W/mEI3mnVi8dmGm743Tp5u2lh0Gr8pyEZHqrLrYm9VV+KQONwme7icXFTjA5RPu9H6uJ/fZ8zRdsJkdbSld6ubQ5Jp9oGng7n/F9qTKsRjMNerQF0/VzAXHuVA3D5m+v081v3ykHwwXQsw5pN5F4Aqqe/Ub227IfeE9UA9M82Sf9hWj+xo7DpYWKzkMVDPlKV2dGofjemzSUkdNiCNQntlYf/8O7z1rmZHWxBfnl+CdGz9ce5pciOfpYVFV+7kixCo18MXPWrInFv2QXsBFBkc75zRps9Hg9aIZrtnHXsb2+5vdfyHtlpob2sdpV1yc0pze0aLnaAQdyDkGAek+R2zmkm2t7IG+6qIvbWgO4DCHaQHNlccp7m1cRvZf15QfdyZkOe9LAJkg6tH5K/Aj5qI9x40lN9KEOnD6JPNPx9tuJ3HGt0o9l7cCdx+HbD0WByf/npsH35pqVGiXFPuJamzPbNrjBshx33+3aDo3FYnuVnvG4wJje5HBwUmpuL+Hq3QeV6Qb2YiVaUPHLXIOK16R6a67pI0hr1yZHQ+Bfxd2ThfP07HYgAG89ht8TmNIA8oVe3Vn37Vx4LzZdFo7GD/Ycgbmec5DHc/24zlHPV/XpBcuj4KfGJbFdnF6j/6wMXla4Cqz2/No2vwzp0/3gxy4rvK+QMMDp8veLEXqXOEKt4zg5DpnhlCcUu1oZZ+AiutQxz1ZGimJUZU8D7RpZ9h+VLm0JbZl4tJPYZnmu0Uu38atZ0fvg/QIxe2TMg762AcDQ/twvKIHh1i27u+59wHKj1BnQF+aZRka2hwsemUGH4OGtbJdnu2hd9isT9Ww/DWe56PDeLuuPj45p84FBNc+Fp+kMo5aYI8Jv7/F1ZrrediBBaOumzviyXA4jbYEbq6+sdDvzQ0dXpgO3wM8Ncy4knGlxp7BLVwPh7+sUkLFh1OiRZ0o5lBWPYkPvBkOAS+d8C4TND49Xq9Axcy5Ls4wuGNQqi7s7SHIQv85gMcPubY0LmtZNHf31DnC2SY1x8U3JDwa5uQlwdDIU1o4lhNOCbxgUg3dTJw52j6h4xzhZXmRW1u5Cm0tkLwlxnkeRIsgh8sXdviiaW6vSL8WR7TKSguocLV9cW1GZogfA3Jf0vRyM3jqM+hzGaiDx2rOxQtv6VMegXNVN7UWM3Imp+lFvEThQQurUQdC15zZXprAHlhddRL57hgpdp2CxReXp3BlBLbVBcpuMNoQVDeEQwHXpSF3mdAWi2WoqWTYAAsvddHASczeIz8/P7+/p/Fnvlyg6JLKS64wmcl9GcvT75/HH6aO1mhMgyDmeyseLZ+HvLAavTFnk1fy+0swv7+xB5+GafED6hJD2YxjhR+rvpzRjMs4fqARN5O5DwZsw8qB2bXP2fPP//7vIgc/TgyewMHm2vPBwvYUi1K8pztPgAGW47jwrPVS1ad3aNOct8z/SbcHjjnZXAvOai+fWnSoyfbCvjhu47LNGFbUVJ1PJb767rk3dc7/QjokYN+8qjUDNxczKFCB4eC5HgnHz3JFvrzxAJRz8Obsi2+pFUtdmvnIjL9/FwFMa+xxh9lcDt3IG/pC0OyHLArsrjM1bQmCVX5/FzpCWKArG72i38y//3vKDGotOsi08YJrQK4rhn2GqekU+AGucpqrumZqX9pKLMt1/HjeA49MaP3fDVSYs8lVbnV7rqs5cz4WuvaME8SJXq3e04t9peai3dGsF2LxclF10fPnAWMu0GhzoZ8dbdkfvBZ+FsxV62+OybArFzOEB3oZXUsj1zOtLpxdTg5HTK5sHdhD80K66XoSGfs/qRfPDjVbDOBoaDFdHmYKNnGGqMsNmDeUk7eurmdYZ1g8xuPvN6XL2WMHh4qt2soMgHHd8s/NDtWei5rnngqvZXHFGXnZ+yFbPCb6P//T4Gwfw4iJHnjQbiN13WZ/vCyI3lNTh9yOFSxdeQjf3RCV6zCX+yQNHU0L2+LM4akHZzhJtuP0RX2Z4Mp+dnPP3nKQPa7KKzNw29x7JOVgpJZTMLn3F2/wzGYwhQ6al/G1Nhq0TyxO6PvE39bkFvZHGZG/f8GL/d09+3Lvu6tOoVGfkQNZdLvPVA/zRD1q7GfsBWnjAR7Tn4zjk+idpbl3o9+DoZPTafpSs/dhWLmYacD2o3Sq9suN9zPy/vsGJ6Ob319mzlD9PFCZZvpoU9cRM0Z/TR1hRcaD9GiDSxy+7eIwmS/BO2Ye+gTnWC7HOyJ9GXcIDTnP3m4ZVx854DRuzG7PGQqN6/57ZeJQ4Tv7xnmPI7G8NIJ5MErb4iqvvuQhlweYpAPFK1P5qv/z6g7MBpdvT2hAnt1q2ezQnp4qWDhyWQupz1YDQJu9tz3aj9lm/lpphfV/1li35yj3x4sxPTlZWPRsXqTAjDO80nt6GW0fhNi8G6JVsiQzpnm25Mm+YzlGXMPTbuopbC50x/KR4ICnHpcVWpxz9t3ZJNBcMve7i2WOo8A0mcebuuTOxkn8/j68izwcfKwCMMrVXkidd16wvOkD28kLoHQK9ui5Wg2LeJ/G8gKzy0trz749HZCl7Line truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,67 @@
<svg width="208" height="120" viewBox="0 0 208 120" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_5291_194117)">
<rect width="208" height="120" rx="6" fill="url(#paint0_radial_5291_194117)"/>
<rect x="22" y="24" width="164" height="72" rx="6" fill="#1A1A1A"/>
<circle cx="33" cy="35" r="3" fill="#2B2B2B"/>
<rect x="38" y="34" width="16" height="2" rx="1" fill="#2B2B2B"/>
<rect x="30" y="42" width="24" height="3" rx="1" fill="#444444"/>
<rect x="30" y="49" width="24" height="2" rx="1" fill="#2B2B2B"/>
<rect x="30" y="55" width="24" height="2" rx="1" fill="#2B2B2B"/>
<rect x="30" y="61" width="24" height="2" rx="1" fill="#2B2B2B"/>
<rect x="30" y="67" width="24" height="2" rx="1" fill="#2B2B2B"/>
<path d="M61 28V92H63V28H61Z" fill="#1C1C1C" mask="url(#path-4-inside-1_5291_194117)"/>
<rect x="70" y="32" width="24" height="2" rx="1" fill="#2B2B2B"/>
<rect x="70" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="80" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="90" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="100" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="110" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="120" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="130" y="38" width="8" height="2" rx="1" fill="#444444"/>
<rect x="90" y="44" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="100" y="44" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="110" y="44" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="120" y="44" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="130" y="44" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="70" y="56" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="80" y="56" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="90" y="56" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="100" y="56" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="110" y="56" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="120" y="56" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="130" y="56" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="70" y="68" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="80" y="68" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="90" y="68" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="100" y="68" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="110" y="68" width="8" height="8" rx="1" fill="#575757"/>
<rect x="120" y="68" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="130" y="68" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="70" y="80" width="8" height="8" rx="1" fill="#1C1C1C"/>
<rect x="80" y="80" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="90" y="80" width="8" height="8" rx="1" fill="#2B2B2B"/>
<rect x="100" y="80" width="8" height="8" rx="1" fill="#2B2B2B"/>
<path d="M145 28V92H147V28H145Z" fill="#1C1C1C" mask="url(#path-13-inside-2_5291_194117)"/>
<rect x="154" y="32" width="24" height="2" rx="1" fill="#2B2B2B"/>
<rect x="154" y="38" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect x="154" y="47" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect x="154" y="56" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect x="154" y="65" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect x="154" y="74" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect x="154" y="83" width="24" height="5" rx="1" fill="#2B2B2B"/>
<rect opacity="0.2" width="208" height="120" fill="url(#pattern0)"/>
</g>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_5291_194117" transform="scale(0.00480769 0.00833333)"/>
</pattern>
<radialGradient id="paint0_radial_5291_194117" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(207.334 -1.47576e-05) rotate(145.398) scale(465.773 828.954)">
<stop stop-color="#282E39"/>
<stop offset="1" stop-color="#D4D4D5"/>
</radialGradient>
<clipPath id="clip0_5291_194117">
<rect width="208" height="120" rx="6" fill="white"/>
</clipPath>
<image id="image0_5291_194117" width="208" height="120" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,149 @@
<svg width="208" height="120" viewBox="0 0 208 120" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_5291_194186)">
<rect width="208" height="120" rx="6" fill="url(#paint0_radial_5291_194186)"/>
<rect x="26.7329" y="13.4136" width="154.535" height="93.1733" rx="5.49505" fill="#1A1A1A"/>
<circle cx="37.1435" cy="23.8247" r="2.74752" fill="#2B2B2B"/>
<rect x="41.7227" y="22.9087" width="14.6535" height="1.83168" rx="0.915842" fill="#2B2B2B"/>
<rect x="34.396" y="30.2354" width="29.3069" height="2.74752" rx="0.915842" fill="#444444"/>
<rect x="34.396" y="36.6465" width="21.9802" height="1.83168" rx="0.915842" fill="#2B2B2B"/>
<rect x="34.396" y="42.1411" width="21.9802" height="1.83168" rx="0.915842" fill="#2B2B2B"/>
<rect x="34.396" y="47.6362" width="21.9802" height="1.83168" rx="0.915842" fill="#2B2B2B"/>
<rect x="34.396" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="41.7227" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="49.0493" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="56.3765" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="63.7031" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="71.0298" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="78.3564" y="58.6265" width="5.49505" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="49.0493" y="64.1216" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="56.3765" y="64.1216" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="63.7031" y="64.1216" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="71.0298" y="64.1216" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="78.3564" y="64.1216" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="34.396" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="41.7227" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="49.0493" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="56.3765" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="63.7031" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="71.0298" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="78.3564" y="73.2798" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="34.396" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="41.7227" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="49.0493" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="56.3765" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="63.7031" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#575757"/>
<rect x="71.0298" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="78.3564" y="82.4385" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="34.396" y="91.5967" width="5.49505" height="5.49505" rx="0.915842" fill="#1C1C1C"/>
<rect x="41.7227" y="91.5967" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="49.0493" y="91.5967" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<rect x="56.3765" y="91.5967" width="5.49505" height="5.49505" rx="0.915842" fill="#2B2B2B"/>
<path d="M92.0943 17.4136V102.587H93.926V17.4136H92.0943Z" fill="#1C1C1C" mask="url(#path-3-inside-1_5291_194186)"/>
<g clip-path="url(#clip1_5291_194186)">
<rect x="100.337" y="21.0771" width="10.9901" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="114.99" y="21.0771" width="10.9901" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="129.644" y="21.0771" width="10.9901" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="144.297" y="21.0771" width="10.9901" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="158.951" y="21.0771" width="10.9901" height="1.83168" rx="0.915842" fill="#444444"/>
<rect x="98.5059" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<path d="M98.5059 28.4039C98.5059 27.3923 99.3259 26.5723 100.338 26.5723H113.159V32.0673H98.5059V28.4039Z" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="113.159" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="127.812" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="142.466" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.466" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="157.12" y="26.5723" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<path d="M157.12 26.5723H169.941C170.953 26.5723 171.773 27.3923 171.773 28.4039V32.0673H157.12V26.5723Z" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="98.5054" y="32.0669" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="32.0669" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="32.0669" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="32.0669" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="32.0669" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="37.562" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="37.562" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="37.562" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="37.562" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="37.562" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="43.0571" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="43.0571" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="43.0571" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="43.0571" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="43.0571" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="48.5522" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="48.5522" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="48.5522" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="48.5522" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="48.5522" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="54.0474" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="54.0474" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="54.0474" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="54.0474" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="54.0474" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="59.5425" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="59.5425" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="59.5425" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="59.5425" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="59.5425" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="65.0371" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="65.0371" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="65.0371" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="65.0371" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="65.0371" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="70.5322" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="70.5322" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="70.5322" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="70.5322" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="70.5322" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="76.0273" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="76.0273" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="76.0273" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="76.0273" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="76.0273" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="81.5225" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="81.5225" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="81.5225" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="81.5225" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="81.5225" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="87.0176" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="87.0176" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="87.0176" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="87.0176" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="157.119" y="87.0176" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="98.5054" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<path d="M98.5054 92.5127H113.159V98.0077H100.337C99.3254 98.0077 98.5054 97.1877 98.5054 96.1761V92.5127Z" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="113.159" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="113.159" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="127.812" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="127.812" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="142.465" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<rect x="142.465" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="157.119" y="92.5127" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
<path d="M157.119 92.5127H171.773V96.1761C171.773 97.1877 170.953 98.0077 169.941 98.0077H157.119V92.5127Z" stroke="#2B2B2B" stroke-width="0.457921"/>
<rect x="98.5054" y="59.5425" width="14.6535" height="10.9901" stroke="#2B2B2B"/>
<rect opacity="0.6" x="127.813" y="75.2798" width="14.6535" height="11.9901" fill="#525252" stroke="#2B2B2B"/>
<rect opacity="0.6" x="157.12" y="59.7104" width="14.6535" height="26.4851" fill="#525252" stroke="#2B2B2B"/>
<rect opacity="0.6" x="99.1196" y="81.5229" width="13.6535" height="15.6726" fill="#525252" stroke="#2B2B2B"/>
<rect opacity="0.6" x="99.1196" y="26.7104" width="13.6535" height="10.4851" fill="#525252" stroke="#2B2B2B"/>
<rect opacity="0.6" x="142.466" y="43.0571" width="14.6535" height="2.74752" fill="#525252" stroke="#2B2B2B"/>
<rect x="113.159" y="26.5718" width="14.6535" height="5.49505" stroke="#2B2B2B"/>
</g>
<rect opacity="0.2" x="0.333008" y="-1" width="208" height="120" fill="url(#pattern0)"/>
</g>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_5291_194186" transform="scale(0.00480769 0.00833333)"/>
</pattern>
<radialGradient id="paint0_radial_5291_194186" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(207.334 -1.47576e-05) rotate(145.398) scale(465.773 828.954)">
<stop stop-color="#282E39"/>
<stop offset="1" stop-color="#D4D4D5"/>
</radialGradient>
<clipPath id="clip0_5291_194186">
<rect width="208" height="120" rx="6" fill="white"/>
</clipPath>
<clipPath id="clip1_5291_194186">
<rect width="84.2574" height="84.2574" fill="white" transform="translate(93.0103 17.4136)"/>
</clipPath>
<image id="image0_5291_194186" width="208" height="120" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 24 KiB

@@ -1408,7 +1408,6 @@
"team_url_required": "يجب إدخال عنوان URL للفريق", "team_url_required": "يجب إدخال عنوان URL للفريق",
"team_url_taken": "عنوان URL هذا مأخوذ بالفعل", "team_url_taken": "عنوان URL هذا مأخوذ بالفعل",
"team_publish": "نشر فريق", "team_publish": "نشر فريق",
"number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)",
"attendee_email_variable": "اسم الحاضر", "attendee_email_variable": "اسم الحاضر",
"attendee_email_info": "البريد الإلكتروني للشخص الحجز", "attendee_email_info": "البريد الإلكتروني للشخص الحجز",
"kbar_search_placeholder": "اكتب أمرًا أو بحثًا...", "kbar_search_placeholder": "اكتب أمرًا أو بحثًا...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Musíte zadat adresu URL týmu", "team_url_required": "Musíte zadat adresu URL týmu",
"team_url_taken": "Tato adresa URL je už obsazená", "team_url_taken": "Tato adresa URL je už obsazená",
"team_publish": "Zveřejnit tým", "team_publish": "Zveřejnit tým",
"number_sms_notifications": "Číslo telefonu (SMS oznámení)",
"attendee_email_variable": "E-mail účastníka", "attendee_email_variable": "E-mail účastníka",
"attendee_email_info": "E-mail osoby provádějící rezervaci", "attendee_email_info": "E-mail osoby provádějící rezervaci",
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...", "kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
@@ -1344,7 +1344,6 @@
"team_url_required": "Der skal indtastes et team URL", "team_url_required": "Der skal indtastes et team URL",
"team_url_taken": "Denne URL er allerede i brug", "team_url_taken": "Denne URL er allerede i brug",
"team_publish": "Publicer team", "team_publish": "Publicer team",
"number_sms_notifications": "Telefonnummer (SMS notifikationer)",
"attendee_email_variable": "Deltagers e-mail", "attendee_email_variable": "Deltagers e-mail",
"attendee_email_info": "Personen der booker's e-mail", "attendee_email_info": "Personen der booker's e-mail",
"kbar_search_placeholder": "Indtast en kommando eller søgning...", "kbar_search_placeholder": "Indtast en kommando eller søgning...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Team-URL muss eingegeben werden", "team_url_required": "Team-URL muss eingegeben werden",
"team_url_taken": "Diese URL ist bereits vergeben", "team_url_taken": "Diese URL ist bereits vergeben",
"team_publish": "Team veröffentlichen", "team_publish": "Team veröffentlichen",
"number_sms_notifications": "Telefonnummer (SMS-Benachrichtigungen)",
"attendee_email_variable": "Teilnehmer E-Mail", "attendee_email_variable": "Teilnehmer E-Mail",
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person", "attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...", "kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
@@ -1466,7 +1466,7 @@
"team_url_required": "Must enter a team URL", "team_url_required": "Must enter a team URL",
"team_url_taken": "This URL is already taken", "team_url_taken": "This URL is already taken",
"team_publish": "Publish team", "team_publish": "Publish team",
"number_sms_notifications": "Phone number (SMS notifications)", "number_sms_notifications": "Phone number (SMS notifications)",
"attendee_email_variable": "Attendee email", "attendee_email_variable": "Attendee email",
"attendee_email_info": "The person booking's email", "attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...", "kbar_search_placeholder": "Type a command or search...",
@@ -1523,6 +1523,9 @@
"no_available_apps": "There are no available apps", "no_available_apps": "There are no available apps",
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'", "no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
"no_apps": "There are no apps enabled in this instance of Cal", "no_apps": "There are no apps enabled in this instance of Cal",
"no_apps_configured": "No app has been configured yet",
"enable_in_settings": "You can enable apps in the settings",
"please_contact_admin": "Please contact your admin",
"apps_settings": "Apps settings", "apps_settings": "Apps settings",
"fill_this_field": "Please fill in this field", "fill_this_field": "Please fill in this field",
"options": "Options", "options": "Options",
@@ -1408,7 +1408,6 @@
"team_url_required": "Debe introducir una URL del equipo", "team_url_required": "Debe introducir una URL del equipo",
"team_url_taken": "Esta URL ya está en uso", "team_url_taken": "Esta URL ya está en uso",
"team_publish": "Publicar equipo", "team_publish": "Publicar equipo",
"number_sms_notifications": "Número de teléfono (notificaciones por SMS)",
"attendee_email_variable": "Correo electrónico del asistente", "attendee_email_variable": "Correo electrónico del asistente",
"attendee_email_info": "El correo electrónico de la persona que reserva", "attendee_email_info": "El correo electrónico de la persona que reserva",
"kbar_search_placeholder": "Escriba un comando o búsqueda...", "kbar_search_placeholder": "Escriba un comando o búsqueda...",
@@ -1523,6 +1523,9 @@
"no_available_apps": "Il n'y a aucune application disponible", "no_available_apps": "Il n'y a aucune application disponible",
"no_available_apps_description": "Veuillez vous assurer qu'il y a des applications dans votre déploiement sous \"packages/app-store\"", "no_available_apps_description": "Veuillez vous assurer qu'il y a des applications dans votre déploiement sous \"packages/app-store\"",
"no_apps": "Aucune application n'est activée dans cette instance de Cal", "no_apps": "Aucune application n'est activée dans cette instance de Cal",
"no_apps_configured": "Aucune application n'a encore été configurée",
"enable_in_settings": "Vous pouvez activer les applications dans les paramètres",
"please_contact_admin": "Veuillez contacter votre administrateur",
"apps_settings": "Paramètres des applications", "apps_settings": "Paramètres des applications",
"fill_this_field": "Veuillez remplir ce champ", "fill_this_field": "Veuillez remplir ce champ",
"options": "Options", "options": "Options",
@@ -1421,7 +1421,6 @@
"team_url_required": "יש להזין כתובת URL של הצוות", "team_url_required": "יש להזין כתובת URL של הצוות",
"team_url_taken": "כתובת ה-URL הזו כבר תפוסה", "team_url_taken": "כתובת ה-URL הזו כבר תפוסה",
"team_publish": "פרסום צוות", "team_publish": "פרסום צוות",
"number_sms_notifications": "מספר טלפון (התראות SMS)",
"attendee_email_variable": "כתובת הדוא\"ל של המשתתף", "attendee_email_variable": "כתובת הדוא\"ל של המשתתף",
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה", "attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...", "kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
@@ -1438,7 +1438,6 @@
"team_url_required": "Immettere un URL per il team", "team_url_required": "Immettere un URL per il team",
"team_url_taken": "URL già occupato", "team_url_taken": "URL già occupato",
"team_publish": "Pubblica team", "team_publish": "Pubblica team",
"number_sms_notifications": "Numero di telefono (notifiche SMS)",
"attendee_email_variable": "E-mail partecipante", "attendee_email_variable": "E-mail partecipante",
"attendee_email_info": "E-mail della persona che prenota", "attendee_email_info": "E-mail della persona che prenota",
"kbar_search_placeholder": "Digita un comando o esegui una ricerca...", "kbar_search_placeholder": "Digita un comando o esegui una ricerca...",
@@ -1408,7 +1408,6 @@
"team_url_required": "チームの URL の入力は必須です", "team_url_required": "チームの URL の入力は必須です",
"team_url_taken": "この URL は既に使用されています", "team_url_taken": "この URL は既に使用されています",
"team_publish": "チームを公開", "team_publish": "チームを公開",
"number_sms_notifications": "電話番号(SMS 通知)",
"attendee_email_variable": "出席者のメールアドレス", "attendee_email_variable": "出席者のメールアドレス",
"attendee_email_info": "予約者のメールアドレス", "attendee_email_info": "予約者のメールアドレス",
"kbar_search_placeholder": "コマンドを入力するか、検索してください...", "kbar_search_placeholder": "コマンドを入力するか、検索してください...",
@@ -1409,7 +1409,6 @@
"team_url_required": "팀 URL을 입력하세요", "team_url_required": "팀 URL을 입력하세요",
"team_url_taken": "이미 사용 중인 URL입니다", "team_url_taken": "이미 사용 중인 URL입니다",
"team_publish": "팀 게시", "team_publish": "팀 게시",
"number_sms_notifications": "전화번호 (SMS notifications)",
"attendee_email_variable": "참석자 이메일", "attendee_email_variable": "참석자 이메일",
"attendee_email_info": "예약자 이메일", "attendee_email_info": "예약자 이메일",
"kbar_search_placeholder": "명령어를 입력하거나 검색하세요...", "kbar_search_placeholder": "명령어를 입력하거나 검색하세요...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Moet een team-URL invoeren", "team_url_required": "Moet een team-URL invoeren",
"team_url_taken": "Deze URL is al in gebruik", "team_url_taken": "Deze URL is al in gebruik",
"team_publish": "Team publiceren", "team_publish": "Team publiceren",
"number_sms_notifications": "Telefoonnummer (sms-meldingen)",
"attendee_email_variable": "E-mailadres deelnemer", "attendee_email_variable": "E-mailadres deelnemer",
"attendee_email_info": "Het e-mailadres van de persoon die boekt", "attendee_email_info": "Het e-mailadres van de persoon die boekt",
"kbar_search_placeholder": "Typ een opdracht of zoek...", "kbar_search_placeholder": "Typ een opdracht of zoek...",
@@ -1318,7 +1318,6 @@
"team_url_required": "Må angi en team URL", "team_url_required": "Må angi en team URL",
"team_url_taken": "Denne URL'en er allerede tatt", "team_url_taken": "Denne URL'en er allerede tatt",
"team_publish": "Publiser team", "team_publish": "Publiser team",
"number_sms_notifications": "Telefonnummer (SMS varsler)",
"attendee_email_variable": "Deltakerens e-post", "attendee_email_variable": "Deltakerens e-post",
"attendee_email_info": "Den som booker sin e-post", "attendee_email_info": "Den som booker sin e-post",
"kbar_search_placeholder": "Skriv inn en kommando eller søk...", "kbar_search_placeholder": "Skriv inn en kommando eller søk...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Musisz wprowadzić adres URL zespołu", "team_url_required": "Musisz wprowadzić adres URL zespołu",
"team_url_taken": "Ten adres URL jest już zajęty", "team_url_taken": "Ten adres URL jest już zajęty",
"team_publish": "Opublikuj zespół", "team_publish": "Opublikuj zespół",
"number_sms_notifications": "Numer telefonu (powiadomienia SMS)",
"attendee_email_variable": "Adres e-mail uczestnika", "attendee_email_variable": "Adres e-mail uczestnika",
"attendee_email_info": "Adres e-mail osoby rezerwującej", "attendee_email_info": "Adres e-mail osoby rezerwującej",
"kbar_search_placeholder": "Wpisz polecenie lub wyszukaj...", "kbar_search_placeholder": "Wpisz polecenie lub wyszukaj...",
@@ -1408,7 +1408,6 @@
"team_url_required": "É preciso inserir uma URL de equipe", "team_url_required": "É preciso inserir uma URL de equipe",
"team_url_taken": "Este URL já está em uso", "team_url_taken": "Este URL já está em uso",
"team_publish": "Publicar equipe", "team_publish": "Publicar equipe",
"number_sms_notifications": "Número de telefone (notificações de SMS)",
"attendee_email_variable": "E-mail do participante", "attendee_email_variable": "E-mail do participante",
"attendee_email_info": "O e-mail da pessoa que fez a reserva", "attendee_email_info": "O e-mail da pessoa que fez a reserva",
"kbar_search_placeholder": "Digite um comando ou procure...", "kbar_search_placeholder": "Digite um comando ou procure...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Deve especificar um endereço para a equipa", "team_url_required": "Deve especificar um endereço para a equipa",
"team_url_taken": "Este endereço já está a ser utilizado", "team_url_taken": "Este endereço já está a ser utilizado",
"team_publish": "Publicar equipa", "team_publish": "Publicar equipa",
"number_sms_notifications": "Número de telefone (notificações SMS)",
"attendee_email_variable": "E-mail do participante", "attendee_email_variable": "E-mail do participante",
"attendee_email_info": "O e-mail do responsável pela reserva", "attendee_email_info": "O e-mail do responsável pela reserva",
"kbar_search_placeholder": "Digite um comando ou pesquise...", "kbar_search_placeholder": "Digite um comando ou pesquise...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Trebuie să introduceți un URL de echipă", "team_url_required": "Trebuie să introduceți un URL de echipă",
"team_url_taken": "Acest URL este folosit deja", "team_url_taken": "Acest URL este folosit deja",
"team_publish": "Publicați echipa", "team_publish": "Publicați echipa",
"number_sms_notifications": "Număr de telefon (notificări SMS)",
"attendee_email_variable": "E-mail participant", "attendee_email_variable": "E-mail participant",
"attendee_email_info": "E-mailul persoanei care efectuează rezervarea", "attendee_email_info": "E-mailul persoanei care efectuează rezervarea",
"kbar_search_placeholder": "Scrie o comandă sau caută...", "kbar_search_placeholder": "Scrie o comandă sau caută...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Необходимо ввести URL-адрес команды", "team_url_required": "Необходимо ввести URL-адрес команды",
"team_url_taken": "Этот URL-адрес уже занят", "team_url_taken": "Этот URL-адрес уже занят",
"team_publish": "Опубликовать команду", "team_publish": "Опубликовать команду",
"number_sms_notifications": "Номер телефона (SMS-уведомления)",
"attendee_email_variable": "Электронная почта участника", "attendee_email_variable": "Электронная почта участника",
"attendee_email_info": "Электронная почта участника, на которого оформляется бронирование", "attendee_email_info": "Электронная почта участника, на которого оформляется бронирование",
"kbar_search_placeholder": "Введите команду или начните поиск...", "kbar_search_placeholder": "Введите команду или начните поиск...",
@@ -1435,7 +1435,6 @@
"team_url_required": "Morate da unesete URL tima", "team_url_required": "Morate da unesete URL tima",
"team_url_taken": "Ovaj URL je zauzet", "team_url_taken": "Ovaj URL je zauzet",
"team_publish": "Objavite tim", "team_publish": "Objavite tim",
"number_sms_notifications": "Broj telefona (obaveštenja putem SMS-a)",
"attendee_email_variable": "Imejl polaznika", "attendee_email_variable": "Imejl polaznika",
"attendee_email_info": "Imejl osobe koja rezerviše", "attendee_email_info": "Imejl osobe koja rezerviše",
"kbar_search_placeholder": "Unesite komandu ili pretražite...", "kbar_search_placeholder": "Unesite komandu ili pretražite...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Team-URL måste anges", "team_url_required": "Team-URL måste anges",
"team_url_taken": "Denna URL används redan", "team_url_taken": "Denna URL används redan",
"team_publish": "Publicera team", "team_publish": "Publicera team",
"number_sms_notifications": "Telefonnummer (SMS-notiser)",
"attendee_email_variable": "Deltagarens e-postadress", "attendee_email_variable": "Deltagarens e-postadress",
"attendee_email_info": "Personens e-postadress för bokning", "attendee_email_info": "Personens e-postadress för bokning",
"kbar_search_placeholder": "Ange ett kommando eller sök ...", "kbar_search_placeholder": "Ange ett kommando eller sök ...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Ekip URL'si girilmelidir", "team_url_required": "Ekip URL'si girilmelidir",
"team_url_taken": "Bu URL zaten kullanılıyor", "team_url_taken": "Bu URL zaten kullanılıyor",
"team_publish": "Ekibi yayınla", "team_publish": "Ekibi yayınla",
"number_sms_notifications": "Telefon numarası (SMS bildirimleri)",
"attendee_email_variable": "Katılımcı e-postası", "attendee_email_variable": "Katılımcı e-postası",
"attendee_email_info": "Rezervasyon yaptıran kişinin e-postası", "attendee_email_info": "Rezervasyon yaptıran kişinin e-postası",
"kbar_search_placeholder": "Komut girin veya arayın...", "kbar_search_placeholder": "Komut girin veya arayın...",
@@ -1408,7 +1408,6 @@
"team_url_required": "Потрібно ввести URL-адресу команди", "team_url_required": "Потрібно ввести URL-адресу команди",
"team_url_taken": "Цю URL-адресу вже зайнято", "team_url_taken": "Цю URL-адресу вже зайнято",
"team_publish": "Опублікувати команду", "team_publish": "Опублікувати команду",
"number_sms_notifications": "Номер телефону (SMS-сповіщення)",
"attendee_email_variable": "Адреса ел. пошти учасника", "attendee_email_variable": "Адреса ел. пошти учасника",
"attendee_email_info": "Адреса ел. пошти особи, яка бронює", "attendee_email_info": "Адреса ел. пошти особи, яка бронює",
"kbar_search_placeholder": "Введіть команду або пошуковий запит…", "kbar_search_placeholder": "Введіть команду або пошуковий запит…",
@@ -1408,7 +1408,6 @@
"team_url_required": "Cần phải nhập một URL của nhóm", "team_url_required": "Cần phải nhập một URL của nhóm",
"team_url_taken": "URL này đã có người lấy rồi", "team_url_taken": "URL này đã có người lấy rồi",
"team_publish": "Đăng nhóm", "team_publish": "Đăng nhóm",
"number_sms_notifications": "Số điện thoại (thông báo SMS)",
"attendee_email_variable": "Email người tham dự", "attendee_email_variable": "Email người tham dự",
"attendee_email_info": "Email người tham gia lịch hẹn", "attendee_email_info": "Email người tham gia lịch hẹn",
"kbar_search_placeholder": "Nhập một lệnh hoặc tìm kiếm...", "kbar_search_placeholder": "Nhập một lệnh hoặc tìm kiếm...",
@@ -1421,7 +1421,6 @@
"team_url_required": "必须输入团队链接", "team_url_required": "必须输入团队链接",
"team_url_taken": "此链接已被占用", "team_url_taken": "此链接已被占用",
"team_publish": "发布团队", "team_publish": "发布团队",
"number_sms_notifications": "电话号码(短信通知)",
"attendee_email_variable": "参与者电子邮件", "attendee_email_variable": "参与者电子邮件",
"attendee_email_info": "预约人的电子邮件", "attendee_email_info": "预约人的电子邮件",
"kbar_search_placeholder": "输入命令或搜索...", "kbar_search_placeholder": "输入命令或搜索...",
@@ -1408,7 +1408,6 @@
"team_url_required": "團隊網址為必填", "team_url_required": "團隊網址為必填",
"team_url_taken": "已有人使用此網址", "team_url_taken": "已有人使用此網址",
"team_publish": "發佈團隊", "team_publish": "發佈團隊",
"number_sms_notifications": "電話號碼 (簡訊通知)",
"attendee_email_variable": "與會者電子郵件", "attendee_email_variable": "與會者電子郵件",
"attendee_email_info": "預約人電子郵件", "attendee_email_info": "預約人電子郵件",
"kbar_search_placeholder": "輸入指令或搜尋…", "kbar_search_placeholder": "輸入指令或搜尋…",
+157
View File
@@ -0,0 +1,157 @@
import { it, expect, describe, beforeAll, afterAll } from "vitest";
let userTypeRouteRegExp: RegExp;
let teamTypeRouteRegExp:RegExp;
let privateLinkRouteRegExp:RegExp
let embedUserTypeRouteRegExp:RegExp
let embedTeamTypeRouteRegExp:RegExp
const getRegExpFromNextJsRewriteRegExp = (nextJsRegExp:string) => {
// const parts = nextJsRegExp.split(':');
// const validNamedGroupRegExp = parts.map((part, index)=>{
// if (index === 0) {
// return part;
// }
// if (part.match(/^[a-zA-Z0-9]+$/)) {
// return `(?<${part}>[^/]+)`
// }
// part = part.replace(new RegExp('([^(]+)(.*)'), '(?<$1>$2)');
// return part
// }).join('');
// TODO: If we can easily convert the exported rewrite regexes from next.config.js to a valid named capturing group regex, it would be best
// Next.js does an exact match as per my testing.
return new RegExp(`^${nextJsRegExp}$`)
}
describe('next.config.js - RegExp', ()=>{
beforeAll(async()=>{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// process.env.NEXTAUTH_SECRET = process.env.NEXTAUTH_URL = process.env.CALENDSO_ENCRYPTION_KEY = 1
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pages = require("../../pages").pages
// How to convert a Next.js rewrite RegExp/wildcard to a valid JS named capturing Group RegExp?
// - /:user/ -> (?<user>[^/]+)
// - /:user(?!404)[^/]+/ -> (?<user>((?!404)[^/]+))
// userTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type((?!book$)[^/]+)`;
userTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp(`/(?<user>((?!${pages.join("/|")})[^/]*))/(?<type>((?!book$)[^/]+))`);
// teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
teamTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp("/team/(?<slug>[^/]+)/(?<type>((?!book$)[^/]+))");
// privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
privateLinkRouteRegExp = getRegExpFromNextJsRewriteRegExp("/d/(?<link>[^/]+)/(?<slug>((?!book$)[^/]+))");
// embedUserTypeRouteRegExp = `/:user((?!${pages.join("|")}).*)/:type/embed`;
embedUserTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp(`/(?<user>((?!${pages.join("|")}).*))/(?<type>[^/]+)/embed`);
// embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
embedTeamTypeRouteRegExp = getRegExpFromNextJsRewriteRegExp("/team/(?<slug>[^/]+)/(?<type>[^/]+)/embed");
});
afterAll(()=>{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
process.env.NEXTAUTH_SECRET = process.env.NEXTAUTH_URL = process.env.CALENDSO_ENCRYPTION_KEY = undefined
})
it("Booking Urls", async () => {
expect(userTypeRouteRegExp.exec('/free/30')?.groups).toContain({
user: 'free',
type: '30'
})
// Edgecase of username starting with team also works
expect(userTypeRouteRegExp.exec('/teampro/30')?.groups).toContain({
user: 'teampro',
type: '30'
})
expect(userTypeRouteRegExp.exec('/teampro+pro/30')?.groups).toContain({
user: 'teampro+pro',
type: '30'
})
expect(userTypeRouteRegExp.exec('/teampro+pro/book')).toEqual(null)
// Because /book doesn't have a corresponding new-booker route.
expect(userTypeRouteRegExp.exec('/free/book')).toEqual(null)
// Because /booked is a normal event name
expect(userTypeRouteRegExp.exec('/free/booked')?.groups).toEqual({
user: 'free',
type: 'booked'
})
expect(embedUserTypeRouteRegExp.exec('/free/30/embed')?.groups).toContain({
user: 'free',
type:'30'
})
expect(teamTypeRouteRegExp.exec('/team/seeded/30')?.groups).toContain({
slug: 'seeded',
type: '30'
})
// Because /book doesn't have a corresponding new-booker route.
expect(teamTypeRouteRegExp.exec('/team/seeded/book')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/team/seeded/30/embed')).toEqual(null)
expect(embedTeamTypeRouteRegExp.exec('/team/seeded/30/embed')?.groups).toContain({
slug: 'seeded',
type:'30'
})
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.groups).toContain({
link: '3v4s321CXRJZx5TFxkpPvd',
slug: '30min'
})
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/30min')?.groups).toContain({
link: '3v4s321CXRJZx5TFxkpPvd',
slug: '30min'
})
// Because /book doesn't have a corresponding new-booker route.
expect(privateLinkRouteRegExp.exec('/d/3v4s321CXRJZx5TFxkpPvd/book')).toEqual(null)
});
it('Non booking Urls', ()=>{
expect(userTypeRouteRegExp.exec('/404')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/404')).toEqual(null)
expect(userTypeRouteRegExp.exec('/404/30')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/404/30')).toEqual(null)
expect(userTypeRouteRegExp.exec('/api')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/api')).toEqual(null)
expect(userTypeRouteRegExp.exec('/api/30')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/api/30')).toEqual(null)
expect(userTypeRouteRegExp.exec('/workflows/30')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/workflows/30')).toEqual(null)
expect(userTypeRouteRegExp.exec('/event-types/30')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/event-types/30')).toEqual(null)
expect(userTypeRouteRegExp.exec('/teams/1')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/teams/1')).toEqual(null)
expect(userTypeRouteRegExp.exec('/teams')).toEqual(null)
expect(teamTypeRouteRegExp.exec('/teams')).toEqual(null)
// Note that even though it matches /embed/embed.js, but it's served from /public and the regexes are in afterEach, it won't hit the flow.
// expect(userTypeRouteRegExp.exec('/embed/embed.js')).toEqual(null)
// expect(teamTypeRouteRegExp.exec('/embed/embed.js')).toEqual(null)
})
})
+7 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useRef, useMemo } from "react";
import StickyBox from "react-sticky-box"; import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import { useEmbedUiConfig } from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
@@ -35,6 +36,7 @@ const BookerComponent = ({
month, month,
rescheduleBooking, rescheduleBooking,
hideBranding = false, hideBranding = false,
isTeamEvent,
}: BookerProps) => { }: BookerProps) => {
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)"); const isTablet = useMediaQuery("(max-width: 1024px)");
@@ -80,6 +82,7 @@ const BookerComponent = ({
rescheduleUid, rescheduleUid,
rescheduleBooking, rescheduleBooking,
layout: defaultLayout, layout: defaultLayout,
isTeamEvent,
}); });
useEffect(() => { useEffect(() => {
@@ -103,6 +106,9 @@ const BookerComponent = ({
} }
}, [layout]); }, [layout]);
const embedUiConfig = useEmbedUiConfig();
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
if (event.isSuccess && !event.data) { if (event.isSuccess && !event.data) {
return <NotFound />; return <NotFound />;
} }
@@ -114,7 +120,7 @@ const BookerComponent = ({
ref={animationScope} ref={animationScope}
className={classNames( className={classNames(
// Sets booker size css variables for the size of all the columns. // Sets booker size css variables for the size of all the columns.
...getBookerSizeClassNames(layout, bookerState), ...getBookerSizeClassNames(layout, bookerState, hideEventTypeDetails),
"bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row", "bg-default dark:bg-muted grid max-w-full items-start dark:[color-scheme:dark] sm:transition-[width] sm:duration-300 sm:motion-reduce:transition-none md:flex-row",
layout === BookerLayouts.MONTH_VIEW && "border-subtle rounded-md border", layout === BookerLayouts.MONTH_VIEW && "border-subtle rounded-md border",
!isEmbed && "sm:transition-[width] sm:duration-300", !isEmbed && "sm:transition-[width] sm:duration-300",
@@ -1,6 +1,7 @@
import { m } from "framer-motion"; import { m } from "framer-motion";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings"; import { EventDetails, EventMembers, EventMetaSkeleton, EventTitle } from "@calcom/features/bookings";
import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details"; import { EventMetaBlock } from "@calcom/features/bookings/components/event-meta/Details";
import { useTimePreferences } from "@calcom/features/bookings/lib"; import { useTimePreferences } from "@calcom/features/bookings/lib";
@@ -25,6 +26,13 @@ export const EventMeta = () => {
const rescheduleBooking = useBookerStore((state) => state.rescheduleBooking); const rescheduleBooking = useBookerStore((state) => state.rescheduleBooking);
const { i18n, t } = useLocale(); const { i18n, t } = useLocale();
const { data: event, isLoading } = useEvent(); const { data: event, isLoading } = useEvent();
const embedUiConfig = useEmbedUiConfig();
const isEmbed = useIsEmbed();
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
if (hideEventTypeDetails) {
return null;
}
return ( return (
<div className="relative z-10 p-6"> <div className="relative z-10 p-6">
+19 -6
View File
@@ -112,25 +112,38 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
}, },
}; };
export const getBookerSizeClassNames = (layout: BookerLayout, bookerState: BookerState) => { export const getBookerSizeClassNames = (
layout: BookerLayout,
bookerState: BookerState,
hideEventTypeDetails = false
) => {
const getBookerMetaClass = (className: string) => {
if (hideEventTypeDetails) {
return "";
}
return className;
};
return [ return [
// Size settings are abstracted on their own lines purely for readbility. // Size settings are abstracted on their own lines purely for readability.
// General sizes, used always // General sizes, used always
"[--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]", "[--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]",
// Small calendar defaults // Small calendar defaults
layout === BookerLayouts.MONTH_VIEW && "[--booker-meta-width:240px]", layout === BookerLayouts.MONTH_VIEW && getBookerMetaClass("[--booker-meta-width:240px]"),
// Meta column get's wider in booking view to fit the full date on a single row in case // Meta column get's wider in booking view to fit the full date on a single row in case
// of a multi occurance event. Also makes form less wide, which also looks better. // of a multi occurance event. Also makes form less wide, which also looks better.
layout === BookerLayouts.MONTH_VIEW && layout === BookerLayouts.MONTH_VIEW &&
bookerState === "booking" && bookerState === "booking" &&
"[--booker-main-width:420px] lg:[--booker-meta-width:340px]", `[--booker-main-width:420px] ${getBookerMetaClass("lg:[--booker-meta-width:340px]")}`,
// Smaller meta when not in booking view. // Smaller meta when not in booking view.
layout === BookerLayouts.MONTH_VIEW && layout === BookerLayouts.MONTH_VIEW &&
bookerState !== "booking" && bookerState !== "booking" &&
"[--booker-main-width:480px] lg:[--booker-meta-width:280px]", `[--booker-main-width:480px] ${getBookerMetaClass("lg:[--booker-meta-width:280px]")}`,
// Fullscreen view settings. // Fullscreen view settings.
layout !== BookerLayouts.MONTH_VIEW && layout !== BookerLayouts.MONTH_VIEW &&
"[--booker-main-width:480px] [--booker-meta-width:340px] lg:[--booker-meta-width:424px]", `[--booker-main-width:480px] [--booker-meta-width:340px] ${getBookerMetaClass(
"lg:[--booker-meta-width:424px]"
)}`,
]; ];
}; };
+32 -2
View File
@@ -22,6 +22,7 @@ type StoreInitializeType = {
rescheduleUid: string | null; rescheduleUid: string | null;
rescheduleBooking: GetBookingType | null | undefined; rescheduleBooking: GetBookingType | null | undefined;
layout: BookerLayout; layout: BookerLayout;
isTeamEvent?: boolean;
}; };
export type BookerStore = { export type BookerStore = {
@@ -89,6 +90,12 @@ export type BookerStore = {
*/ */
formValues: Record<string, any>; formValues: Record<string, any>;
setFormValues: (values: Record<string, any>) => void; setFormValues: (values: Record<string, any>) => void;
/**
* Force event being a team event, so we only query for team events instead
* of also include 'user' events and return the first event that matches with
* both the slug and the event slug.
*/
isTeamEvent: boolean;
}; };
/** /**
@@ -137,6 +144,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
updateQueryParam("month", month ?? ""); updateQueryParam("month", month ?? "");
get().setSelectedDate(null); get().setSelectedDate(null);
}, },
isTeamEvent: false,
initialize: ({ initialize: ({
username, username,
eventSlug, eventSlug,
@@ -145,6 +153,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
rescheduleUid = null, rescheduleUid = null,
rescheduleBooking = null, rescheduleBooking = null,
layout, layout,
isTeamEvent,
}: StoreInitializeType) => { }: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate; const selectedDateInStore = get().selectedDate;
@@ -165,6 +174,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
rescheduleUid, rescheduleUid,
rescheduleBooking, rescheduleBooking,
layout: layout || BookerLayouts.MONTH_VIEW, layout: layout || BookerLayouts.MONTH_VIEW,
isTeamEvent: isTeamEvent || false,
// Preselect today's date in week / column view, since they use this to show the week title. // Preselect today's date in week / column view, since they use this to show the week title.
selectedDate: selectedDate:
selectedDateInStore || selectedDateInStore ||
@@ -207,9 +217,29 @@ export const useInitializeBookerStore = ({
rescheduleUid = null, rescheduleUid = null,
rescheduleBooking = null, rescheduleBooking = null,
layout, layout,
isTeamEvent,
}: StoreInitializeType) => { }: StoreInitializeType) => {
const initializeStore = useBookerStore((state) => state.initialize); const initializeStore = useBookerStore((state) => state.initialize);
useEffect(() => { useEffect(() => {
initializeStore({ username, eventSlug, month, eventId, rescheduleUid, rescheduleBooking, layout }); initializeStore({
}, [initializeStore, username, eventSlug, month, eventId, rescheduleUid, rescheduleBooking, layout]); username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduleBooking,
layout,
isTeamEvent,
});
}, [
initializeStore,
username,
eventSlug,
month,
eventId,
rescheduleUid,
rescheduleBooking,
layout,
isTeamEvent,
]);
}; };
@@ -41,6 +41,13 @@ export interface BookerProps {
* within the atom (i.e. without a server side component). * within the atom (i.e. without a server side component).
*/ */
rescheduleBooking?: GetBookingType; rescheduleBooking?: GetBookingType;
/**
* If this boolean is passed, we will only check team events with this slug and event slug.
* If it's not passed, we will first query a generic user event, and only if that doesn't exist
* fetch the team event. In case there's both a team + user with the same slug AND same event slug,
* that will always result in the user event being returned.
*/
isTeamEvent?: boolean;
} }
export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking"; export type BookerState = "loading" | "selecting_date" | "selecting_time" | "booking";
@@ -16,9 +16,10 @@ import { useBookerStore } from "../store";
*/ */
export const useEvent = () => { export const useEvent = () => {
const [username, eventSlug] = useBookerStore((state) => [state.username, state.eventSlug], shallow); const [username, eventSlug] = useBookerStore((state) => [state.username, state.eventSlug], shallow);
const isTeamEvent = useBookerStore((state) => state.isTeamEvent);
return trpc.viewer.public.event.useQuery( return trpc.viewer.public.event.useQuery(
{ username: username ?? "", eventSlug: eventSlug ?? "" }, { username: username ?? "", eventSlug: eventSlug ?? "", isTeamEvent },
{ refetchOnWindowFocus: false, enabled: Boolean(username) && Boolean(eventSlug) } { refetchOnWindowFocus: false, enabled: Boolean(username) && Boolean(eventSlug) }
); );
}; };
@@ -0,0 +1,81 @@
import type { Session } from "next-auth";
import { describe, expect, it } from "vitest";
import { UserPermissionRole } from "@calcom/prisma/enums";
import {
parseTeamId,
checkSelfImpersonation,
checkUserIdentifier,
checkPermission,
} from "./ImpersonationProvider";
const session: Session = {
expires: "2021-08-31T15:00:00.000Z",
hasValidLicense: true,
user: {
id: 123,
username: "test",
role: UserPermissionRole.USER,
email: "test@example.com",
},
};
describe("parseTeamId", () => {
it("should return undefined if no teamId is provided", () => {
expect(parseTeamId(undefined)).toBeUndefined();
});
it("should return the parsed teamId if a teamId is provided", () => {
expect(parseTeamId({ username: "test", teamId: "123" })).toBe(123);
});
it("should throw an error if the provided teamId is not a positive number", () => {
expect(() => parseTeamId({ username: "test", teamId: "-123" })).toThrow();
});
it("should throw an error if the provided teamId is not a number", () => {
expect(() => parseTeamId({ username: "test", teamId: "notanumber" })).toThrow();
});
});
describe("checkSelfImpersonation", () => {
it("should throw an error if the provided username is the same as the session user's username", () => {
expect(() => checkSelfImpersonation(session, { username: "test" })).toThrow();
});
it("should throw an error if the provided username is the same as the session user's email", () => {
expect(() => checkSelfImpersonation(session, { username: "test@example.com" })).toThrow();
});
it("should not throw an error if the provided username is different from the session user's username and email", () => {
expect(() => checkSelfImpersonation(session, { username: "other" })).not.toThrow();
});
});
describe("checkUserIdentifier", () => {
it("should throw an error if no username is provided", () => {
expect(() => checkUserIdentifier(undefined)).toThrow();
});
it("should not throw an error if a username is provided", () => {
expect(() => checkUserIdentifier({ username: "test" })).not.toThrow();
});
});
describe("checkPermission", () => {
it("should throw an error if the user is not an admin and team impersonation is disabled", () => {
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "false";
expect(() => checkPermission(session)).toThrow();
});
it("should not throw an error if the user is an admin and team impersonation is disabled", () => {
const modifiedSession = { ...session, user: { ...session.user, role: UserPermissionRole.ADMIN } };
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "false";
expect(() => checkPermission(modifiedSession)).not.toThrow();
});
it("should not throw an error if the user is not an admin but team impersonation is enabled", () => {
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION = "true";
expect(() => checkPermission(session)).not.toThrow();
});
});
@@ -1,4 +1,5 @@
import type { User } from "@prisma/client"; import type { User } from "@prisma/client";
import type { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import { z } from "zod"; import { z } from "zod";
@@ -44,6 +45,28 @@ const auditAndReturnNextUser = async (
return obj; return obj;
}; };
type Credentials = Record<"username" | "teamId", string> | undefined;
export function parseTeamId(creds: Partial<Credentials>) {
return creds?.teamId ? teamIdschema.parse({ teamId: creds.teamId }).teamId : undefined;
}
export function checkSelfImpersonation(session: Session | null, creds: Partial<Credentials>) {
if (session?.user.username === creds?.username || session?.user.email === creds?.username) {
throw new Error("You cannot impersonate yourself.");
}
}
export function checkUserIdentifier(creds: Partial<Credentials>) {
if (!creds?.username) throw new Error("User identifier must be present");
}
export function checkPermission(session: Session | null) {
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
throw new Error("You do not have permission to do this.");
}
}
const ImpersonationProvider = CredentialsProvider({ const ImpersonationProvider = CredentialsProvider({
id: "impersonation-auth", id: "impersonation-auth",
name: "Impersonation", name: "Impersonation",
@@ -56,18 +79,10 @@ const ImpersonationProvider = CredentialsProvider({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore need to figure out how to correctly type this // @ts-ignore need to figure out how to correctly type this
const session = await getSession({ req }); const session = await getSession({ req });
// If teamId is present -> parse the teamId and throw error itn ot number. If not present teamId is set to undefined const teamId = parseTeamId(creds);
const teamId = creds?.teamId ? teamIdschema.parse({ teamId: creds.teamId }).teamId : undefined; checkSelfImpersonation(session, creds);
checkUserIdentifier(creds);
if (session?.user.username === creds?.username || session?.user.email === creds?.username) { checkPermission(session);
throw new Error("You cannot impersonate yourself.");
}
if (!creds?.username) throw new Error("User identifier must be present");
// If you are an ADMIN we return way before team impersonation logic is executed, so NEXT_PUBLIC_TEAM_IMPERSONATION certainly true
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
throw new Error("You do not have permission to do this.");
}
// Get user who is being impersonated // Get user who is being impersonated
const impersonatedUser = await prisma.user.findFirst({ const impersonatedUser = await prisma.user.findFirst({
@@ -1,118 +0,0 @@
import { useSession } from "next-auth/react";
import type { ReactNode, InputHTMLAttributes } from "react";
import { useState, forwardRef, Fragment } from "react";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar } from "@calcom/ui";
import { Layers, User } from "@calcom/ui/components/icon";
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
export type IEventTypeFilter = IEventTypesFilters[0];
export const OrganizationEventTypeFilter = () => {
const { t } = useLocale();
const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
const [dropdownTitle, setDropdownTitle] = useState<string>(t("all_apps"));
const { data: teams, status } = trpc.viewer.teams.list.useQuery();
const isNotEmpty = !!teams?.length;
return status === "success" ? (
<AnimatedPopover text={dropdownTitle} popoverTriggerClassNames="!mb-0">
<CheckboxFieldContainer>
<CheckboxField
id="all-eventtypes-checkbox"
icon={<Layers className="h-4 w-4" />}
checked={dropdownTitle === t("all_apps")}
onChange={(e) => {
removeAllQueryParams();
setDropdownTitle(t("all_apps"));
// TODO: What to do when all event types is unchecked
}}
label={t("all_apps")}
/>
</CheckboxFieldContainer>
<CheckboxFieldContainer>
<CheckboxField
id="all-eventtypes-checkbox"
icon={<User className="h-4 w-4" />}
checked={query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
setDropdownTitle(t("yours"));
if (e.target.checked) {
pushItemToKey("userIds", session.data?.user.id || 0);
} else if (!e.target.checked) {
removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
}
}}
label={t("yours")}
/>
</CheckboxFieldContainer>
{isNotEmpty && (
<Fragment>
<div className="text-subtle px-4 py-2.5 text-xs font-medium uppercase leading-none">TEAMS</div>
{teams?.map((team) => (
<CheckboxFieldContainer key={team.id}>
<CheckboxField
id={team.name}
label={team.name}
icon={
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
checked={query.teamIds?.includes(team.id)}
onChange={(e) => {
setDropdownTitle(team.name);
if (e.target.checked) {
pushItemToKey("teamIds", team.id);
} else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id);
}
}}
/>
</CheckboxFieldContainer>
))}
</Fragment>
)}
</AnimatedPopover>
) : null;
};
type Props = InputHTMLAttributes<HTMLInputElement> & {
label: string;
icon: ReactNode;
};
const CheckboxField = forwardRef<HTMLInputElement, Props>(({ label, icon, ...rest }, ref) => {
return (
<label className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-default flex h-6 w-6 items-center justify-center ltr:mr-2 rtl:ml-2">{icon}</div>
<span className="text-sm">{label}</span>
</div>
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded hover:cursor-pointer"
/>
</div>
</label>
);
});
const CheckboxFieldContainer = ({ children }: { children: ReactNode }) => {
return <div className="flex items-center px-3 py-2">{children}</div>;
};
CheckboxField.displayName = "CheckboxField";
@@ -7,7 +7,7 @@ import { getAppFromSlug } from "@calcom/app-store/utils";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib"; import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { PrismaClient } from "@calcom/prisma/client"; import type { PrismaClient } from "@calcom/prisma/client";
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils"; import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
@@ -71,13 +71,20 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
name: true, name: true,
theme: true, theme: true,
metadata: true, metadata: true,
brandColor: true,
darkBrandColor: true,
}, },
}, },
hidden: true, hidden: true,
}); });
export const getPublicEvent = async (username: string, eventSlug: string, prisma: PrismaClient) => { export const getPublicEvent = async (
const usernameList = username.split("+"); username: string,
eventSlug: string,
isTeamEvent: boolean | undefined,
prisma: PrismaClient
) => {
const usernameList = getUsernameList(username);
// In case of dynamic group event, we fetch user's data and use the default event. // In case of dynamic group event, we fetch user's data and use the default event.
if (usernameList.length > 1) { if (usernameList.length > 1) {
@@ -141,24 +148,26 @@ export const getPublicEvent = async (username: string, eventSlug: string, prisma
}; };
} }
const usersOrTeamQuery = isTeamEvent
? {
team: {
slug: username,
},
}
: {
users: {
some: {
username,
},
},
team: null,
};
// In case it's not a group event, it's either a single user or a team, and we query that data. // 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({ const event = await prisma.eventType.findFirst({
where: { where: {
slug: eventSlug, slug: eventSlug,
OR: [ ...usersOrTeamQuery,
{
users: {
some: {
username,
},
},
},
{
team: {
slug: username,
},
},
],
}, },
select: publicEventSelect, select: publicEventSelect,
}); });
@@ -7,7 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, Divider } from "@calcom/ui"; import { AnimatedPopover, Avatar, Divider, VerticalDivider } from "@calcom/ui";
import { Layers, User } from "@calcom/ui/components/icon"; import { Layers, User } from "@calcom/ui/components/icon";
import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery"; import { filterQuerySchema } from "../lib/getTeamsFiltersFromQuery";
@@ -20,7 +20,13 @@ function useFilterQuery() {
return useTypedQuery(filterQuerySchema.passthrough()); return useTypedQuery(filterQuerySchema.passthrough());
} }
export const TeamsFilter = () => { export const TeamsFilter = ({
popoverTriggerClassNames,
showVerticalDivider = false,
}: {
popoverTriggerClassNames?: string;
showVerticalDivider?: boolean;
}) => {
const { t } = useLocale(); const { t } = useLocale();
const session = useSession(); const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery(); const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
@@ -40,7 +46,7 @@ export const TeamsFilter = () => {
if (selectedTeamsNames) { if (selectedTeamsNames) {
checkedOptions.push(...selectedTeamsNames); checkedOptions.push(...selectedTeamsNames);
} }
return checkedOptions.join(","); return `${t("team")}: ${checkedOptions.join(",")}`;
} }
if (query.userIds) { if (query.userIds) {
return t("yours"); return t("yours");
@@ -51,56 +57,59 @@ export const TeamsFilter = () => {
if (!teams || !teams.length) return null; if (!teams || !teams.length) return null;
return ( return (
<AnimatedPopover text={getCheckedOptionsNames()}> <div className="flex items-center">
<FilterCheckboxFieldsContainer> <AnimatedPopover text={getCheckedOptionsNames()} popoverTriggerClassNames={popoverTriggerClassNames}>
<FilterCheckboxField <FilterCheckboxFieldsContainer>
id="all"
icon={<Layers className="h-4 w-4" />}
checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
removeAllQueryParams();
}}
label={t("all_apps")}
/>
<FilterCheckboxField
id="yours"
icon={<User className="h-4 w-4" />}
checked={!!query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
if (e.target.checked) {
pushItemToKey("userIds", session.data?.user.id || 0);
} else if (!e.target.checked) {
removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
}
}}
label={t("yours")}
/>
<Divider />
{teams?.map((team) => (
<FilterCheckboxField <FilterCheckboxField
key={team.id} id="all"
id={team.name} icon={<Layers className="h-4 w-4" />}
label={team.name} checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
checked={!!query.teamIds?.includes(team.id)} onChange={(e) => {
removeAllQueryParams();
}}
label={t("all_apps")}
/>
<FilterCheckboxField
id="yours"
icon={<User className="h-4 w-4" />}
checked={!!query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => { onChange={(e) => {
if (e.target.checked) { if (e.target.checked) {
pushItemToKey("teamIds", team.id); pushItemToKey("userIds", session.data?.user.id || 0);
} else if (!e.target.checked) { } else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id); removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
} }
}} }}
icon={ label={t("yours")}
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
/> />
))} <Divider />
</FilterCheckboxFieldsContainer> {teams?.map((team) => (
</AnimatedPopover> <FilterCheckboxField
key={team.id}
id={team.name}
label={team.name}
checked={!!query.teamIds?.includes(team.id)}
onChange={(e) => {
if (e.target.checked) {
pushItemToKey("teamIds", team.id);
} else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id);
}
}}
icon={
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
/>
))}
</FilterCheckboxFieldsContainer>
</AnimatedPopover>
{showVerticalDivider && <VerticalDivider />}
</div>
); );
}; };
@@ -1,4 +1,5 @@
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
type UseScheduleWithCacheArgs = { type UseScheduleWithCacheArgs = {
@@ -22,13 +23,12 @@ export const useSchedule = ({
}: UseScheduleWithCacheArgs) => { }: UseScheduleWithCacheArgs) => {
const monthDayjs = month ? dayjs(month) : dayjs(); const monthDayjs = month ? dayjs(month) : dayjs();
const nextMonthDayjs = monthDayjs.add(1, "month"); const nextMonthDayjs = monthDayjs.add(1, "month");
// Why the non-null assertions? All of these arguments are checked in the enabled condition, // 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 // and the query will not run if they are null. However, the check in `enabled` does
// no satisfy typscript. // no satisfy typscript.
return trpc.viewer.public.slots.getSchedule.useQuery( return trpc.viewer.public.slots.getSchedule.useQuery(
{ {
usernameList: username && username.indexOf("+") > -1 ? username.split("+") : [username!], usernameList: getUsernameList(username ?? ""),
eventTypeSlug: eventSlug!, eventTypeSlug: eventSlug!,
// @TODO: Old code fetched 2 days ago if we were fetching the current month. // @TODO: Old code fetched 2 days ago if we were fetching the current month.
// Do we want / need to keep that behavior? // Do we want / need to keep that behavior?
@@ -23,6 +23,13 @@ type BookerLayoutSelectorProps = {
* settings. * settings.
*/ */
fallbackToUserSettings?: boolean; fallbackToUserSettings?: boolean;
/**
* isDark boolean should be passed in when the user selected 'dark mode' in the theme settings in profile/appearance.
* So it's not based on the user's system settings, but on the user's preference for the booker.
* This boolean is then used to show a dark version of the layout image. It's only easthetic, no functionality is attached
* to this boolean.
*/
isDark?: boolean;
}; };
const defaultFieldName = "metadata.bookerLayouts"; const defaultFieldName = "metadata.bookerLayouts";
@@ -32,6 +39,7 @@ export const BookerLayoutSelector = ({
description, description,
name, name,
fallbackToUserSettings, fallbackToUserSettings,
isDark,
}: BookerLayoutSelectorProps) => { }: BookerLayoutSelectorProps) => {
const { control, getValues } = useFormContext(); const { control, getValues } = useFormContext();
const { t } = useLocale(); const { t } = useLocale();
@@ -58,6 +66,7 @@ export const BookerLayoutSelector = ({
showUserSettings={shouldShowUserSettings} showUserSettings={shouldShowUserSettings}
settings={value} settings={value}
onChange={onChange} onChange={onChange}
isDark={isDark}
/> />
)} )}
/> />
@@ -69,11 +78,12 @@ type BookerLayoutFieldsProps = {
settings: BookerLayoutSettings; settings: BookerLayoutSettings;
onChange: (settings: BookerLayoutSettings) => void; onChange: (settings: BookerLayoutSettings) => void;
showUserSettings: boolean; showUserSettings: boolean;
isDark?: boolean;
}; };
type BookerLayoutState = { [key in BookerLayouts]: boolean }; type BookerLayoutState = { [key in BookerLayouts]: boolean };
const BookerLayoutFields = ({ settings, onChange, showUserSettings }: BookerLayoutFieldsProps) => { const BookerLayoutFields = ({ settings, onChange, showUserSettings, isDark }: BookerLayoutFieldsProps) => {
const { t } = useLocale(); const { t } = useLocale();
const { isLoading: isUserLoading, data: user } = useMeQuery(); const { isLoading: isUserLoading, data: user } = useMeQuery();
const [isOverridingSettings, setIsOverridingSettings] = useState(false); const [isOverridingSettings, setIsOverridingSettings] = useState(false);
@@ -143,7 +153,7 @@ const BookerLayoutFields = ({ settings, onChange, showUserSettings }: BookerLayo
<label> <label>
<img <img
className="mb-3 w-full max-w-none cursor-pointer" className="mb-3 w-full max-w-none cursor-pointer"
src={`/bookerlayout_${layout}.svg`} src={`/bookerlayout_${layout}${isDark ? "_dark" : ""}.svg`}
alt="Layout preview" alt="Layout preview"
/> />
<Checkbox <Checkbox
+2 -2
View File
@@ -24,9 +24,9 @@
}, },
"dependencies": { "dependencies": {
"@calcom/lib": "*", "@calcom/lib": "*",
"@prisma/client": "^4.13.0", "@prisma/client": "^4.16.0",
"@prisma/generator-helper": "^4.13.0", "@prisma/generator-helper": "^4.13.0",
"prisma": "^4.13.0", "prisma": "^4.16.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"zod": "^3.20.2", "zod": "^3.20.2",
"zod-prisma": "^0.5.4" "zod-prisma": "^0.5.4"
@@ -10,6 +10,6 @@ interface EventHandlerOptions {
} }
export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => { export const eventHandler = async ({ ctx, input }: EventHandlerOptions) => {
const event = await getPublicEvent(input.username, input.eventSlug, ctx.prisma); const event = await getPublicEvent(input.username, input.eventSlug, input.isTeamEvent, ctx.prisma);
return event; return event;
}; };
@@ -3,6 +3,7 @@ import z from "zod";
export const ZEventInputSchema = z.object({ export const ZEventInputSchema = z.object({
username: z.string(), username: z.string(),
eventSlug: z.string(), eventSlug: z.string(),
isTeamEvent: z.boolean().optional(),
}); });
export type TEventInputSchema = z.infer<typeof ZEventInputSchema>; export type TEventInputSchema = z.infer<typeof ZEventInputSchema>;
@@ -8,6 +8,7 @@ import { ZCreateInputSchema } from "./create.schema";
import { ZDeleteInputSchema } from "./delete.schema"; import { ZDeleteInputSchema } from "./delete.schema";
import { ZDuplicateInputSchema } from "./duplicate.schema"; import { ZDuplicateInputSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema"; import { ZGetInputSchema } from "./get.schema";
import { ZEventTypeInputSchema } from "./getByViewer.schema";
import { ZUpdateInputSchema } from "./update.schema"; import { ZUpdateInputSchema } from "./update.schema";
import { eventOwnerProcedure } from "./util"; import { eventOwnerProcedure } from "./util";
@@ -28,7 +29,7 @@ const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {};
export const eventTypesRouter = router({ export const eventTypesRouter = router({
// REVIEW: What should we name this procedure? // REVIEW: What should we name this procedure?
getByViewer: authedProcedure.query(async ({ ctx }) => { getByViewer: authedProcedure.input(ZEventTypeInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.getByViewer) { if (!UNSTABLE_HANDLER_CACHE.getByViewer) {
UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then( UNSTABLE_HANDLER_CACHE.getByViewer = await import("./getByViewer.handler").then(
(mod) => mod.getByViewerHandler (mod) => mod.getByViewerHandler
@@ -44,6 +45,7 @@ export const eventTypesRouter = router({
const result = await UNSTABLE_HANDLER_CACHE.getByViewer({ const result = await UNSTABLE_HANDLER_CACHE.getByViewer({
ctx, ctx,
input,
}); });
timer(); timer();
@@ -1,6 +1,7 @@
import { type PrismaClient, Prisma } from "@prisma/client"; import { type PrismaClient, Prisma } from "@prisma/client";
import { orderBy } from "lodash"; import { orderBy } from "lodash";
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { CAL_URL } from "@calcom/lib/constants"; import { CAL_URL } from "@calcom/lib/constants";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma"; import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma";
@@ -11,53 +12,58 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc"; import type { TrpcSessionUser } from "../../../trpc";
import type { TEventTypeInputSchema } from "./getByViewer.schema";
type GetByViewerOptions = { type GetByViewerOptions = {
ctx: { ctx: {
user: NonNullable<TrpcSessionUser>; user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient; prisma: PrismaClient;
}; };
input: TEventTypeInputSchema;
}; };
export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => { const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
// Position is required by lodash to sort on it. Don't remove it, TS won't complain but it would silently break reordering
position: true,
hashedLink: true,
locations: true,
destinationCalendar: true,
userId: true,
team: {
select: {
id: true,
name: true,
slug: true,
// logo: true, // Skipping to avoid 4mb limit
bio: true,
hideBranding: true,
},
},
metadata: true,
users: {
select: baseUserSelect,
},
children: {
include: {
users: {
select: baseUserSelect,
},
},
},
parentId: true,
hosts: {
select: {
user: {
select: baseUserSelect,
},
},
},
seatsPerTimeSlot: true,
...baseEventTypeSelect,
});
export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) => {
const { prisma } = ctx; const { prisma } = ctx;
const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
// Position is required by lodash to sort on it. Don't remove it, TS won't complain but it would silently break reordering
position: true,
hashedLink: true,
locations: true,
destinationCalendar: true,
userId: true,
team: {
select: {
id: true,
name: true,
slug: true,
// logo: true, // Skipping to avoid 4mb limit
bio: true,
hideBranding: true,
},
},
metadata: true,
users: {
select: baseUserSelect,
},
children: {
include: {
users: true,
},
},
parentId: true,
hosts: {
select: {
user: {
select: baseUserSelect,
},
},
},
seatsPerTimeSlot: true,
...baseEventTypeSelect,
});
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
@@ -107,6 +113,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
eventTypes: { eventTypes: {
where: { where: {
team: null, team: null,
userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
}, },
select: eventTypeSelect, select: eventTypeSelect,
orderBy: [ orderBy: [
@@ -137,7 +144,8 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
const typesRaw = ( const typesRaw = (
await prisma.eventType.findMany({ await prisma.eventType.findMany({
where: { where: {
userId: ctx.user.id, userId: getPrismaWhereUserIdFromFilter(ctx.user.id, input?.filters),
team: null,
}, },
select: eventTypeSelect, select: eventTypeSelect,
orderBy: [ orderBy: [
@@ -167,11 +175,13 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
}; };
let eventTypeGroups: EventTypeGroup[] = []; let eventTypeGroups: EventTypeGroup[] = [];
const eventTypesHashMap = userEventTypes.concat(typesRaw).reduce((hashMap, newItem) => { const eventTypesHashMap = userEventTypes.concat(typesRaw).reduce((hashMap, newItem) => {
const oldItem = hashMap[newItem.id]; const oldItem = hashMap[newItem.id];
hashMap[newItem.id] = { ...oldItem, ...newItem }; hashMap[newItem.id] = { ...oldItem, ...newItem };
return hashMap; return hashMap;
}, {} as Record<number, EventTypeGroup["eventTypes"][number]>); }, {} as Record<number, EventTypeGroup["eventTypes"][number]>);
const mergedEventTypes = Object.values(eventTypesHashMap) const mergedEventTypes = Object.values(eventTypesHashMap)
.map((eventType) => eventType) .map((eventType) => eventType)
.filter((evType) => evType.schedulingType !== SchedulingType.MANAGED); .filter((evType) => evType.schedulingType !== SchedulingType.MANAGED);
@@ -204,6 +214,13 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
return mshipToNumber(mship1) > mshipToNumber(mship2); return mshipToNumber(mship1) > mshipToNumber(mship2);
}; };
const filterTeamsEventTypesBasedOnInput = (eventType: ReturnType<typeof mapEventType>) => {
if (!input?.filters || !hasFilter(input?.filters)) {
return true;
}
return input?.filters?.teamIds?.includes(eventType?.team?.id || 0) ?? false;
};
eventTypeGroups = ([] as EventTypeGroup[]).concat( eventTypeGroups = ([] as EventTypeGroup[]).concat(
eventTypeGroups, eventTypeGroups,
user.teams user.teams
@@ -242,6 +259,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
}, },
eventTypes: membership.team.eventTypes eventTypes: membership.team.eventTypes
.map(mapEventType) .map(mapEventType)
.filter(filterTeamsEventTypesBasedOnInput)
.filter((evType) => evType.userId === null || evType.userId === ctx.user.id) .filter((evType) => evType.userId === null || evType.userId === ctx.user.id)
.filter((evType) => .filter((evType) =>
membership.role === MembershipRole.MEMBER membership.role === MembershipRole.MEMBER
@@ -251,6 +269,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
}; };
}) })
); );
return { return {
// don't display event teams without event types, // don't display event teams without event types,
eventTypeGroups: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length), eventTypeGroups: eventTypeGroups.filter((groupBy) => !!groupBy.eventTypes?.length),
@@ -264,3 +283,15 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
})), })),
}; };
}; };
export function getPrismaWhereUserIdFromFilter(
userId: number,
filters: NonNullable<TEventTypeInputSchema>["filters"]
) {
if (!filters || !hasFilter(filters)) {
return userId;
} else if (filters.userIds?.[0] === userId) {
return userId;
}
return null;
}
@@ -1 +1,15 @@
export {}; import { z } from "zod";
export const filterQuerySchemaStrict = z.object({
teamIds: z.number().array().optional(),
// A user can only filter by only his userId
userIds: z.number().array().max(1).optional(),
});
export const ZEventTypeInputSchema = z
.object({
filters: filterQuerySchemaStrict.optional(),
})
.nullish();
export type TEventTypeInputSchema = z.infer<typeof ZEventTypeInputSchema>;
@@ -1,8 +1,10 @@
import { subdomainSuffix } from "@calcom/ee/organizations/lib/orgDomains";
import { cancelTeamSubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments"; import { cancelTeamSubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import { IS_PRODUCTION, IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { isTeamOwner } from "@calcom/lib/server/queries/teams"; import { isTeamOwner } from "@calcom/lib/server/queries/teams";
import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager"; import { closeComDeleteTeam } from "@calcom/lib/sync/SyncServiceManager";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
@@ -16,6 +18,41 @@ type DeleteOptions = {
input: TDeleteInputSchema; input: TDeleteInputSchema;
}; };
const deleteVercelDomain = async ({
slug,
isOrganization,
}: {
slug?: string | null;
isOrganization?: boolean | null;
}) => {
if (!isOrganization || !slug) {
return false;
}
const fullDomain = `${slug}.${subdomainSuffix()}`;
const response = await fetch(
`https://api.vercel.com/v9/projects/${process.env.PROJECT_ID_VERCEL}/domains/${fullDomain}?teamId=${process.env.TEAM_ID_VERCEL}`,
{
headers: {
Authorization: `Bearer ${process.env.AUTH_BEARER_TOKEN_VERCEL}`,
},
method: "DELETE",
}
);
const data = await response.json();
// Domain is already owned by another team but you can request delegation to access it
if (data.error?.code === "forbidden")
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_team" });
// Domain is already being used by a different project
if (data.error?.code === "domain_taken")
throw new TRPCError({ code: "CONFLICT", message: "domain_taken_project" });
return true;
};
export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
if (!(await isTeamOwner(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" }); if (!(await isTeamOwner(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -34,6 +71,14 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => {
}, },
}); });
const deletedTeamMetadata = teamMetadataSchema.parse(deletedTeam.metadata);
if (IS_PRODUCTION)
deleteVercelDomain({
slug: deletedTeam.slug,
isOrganization: deletedTeamMetadata?.isOrganization,
});
// Sync Services: Close.cm // Sync Services: Close.cm
closeComDeleteTeam(deletedTeam); closeComDeleteTeam(deletedTeam);
}; };
+98 -30
View File
@@ -1,4 +1,6 @@
// @TODO: turn this into a more generic component that has the same Props API as MUI https://mui.com/material-ui/react-card/ // @TODO: turn this into a more generic component that has the same Props API as MUI https://mui.com/material-ui/react-card/
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import Link from "next/link"; import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import React from "react"; import React from "react";
@@ -8,10 +10,96 @@ import { ArrowRight } from "@calcom/ui/components/icon";
import { Button } from "../button"; import { Button } from "../button";
export type BaseCardProps = { const cvaCardTypeByVariant = cva("", {
// Variants won't have any style by default. Style will only be applied if the variants are combined.
// So, style is defined in compoundVariants.
variants: {
variant: {
basic: "",
ProfileCard: "",
SidebarCard: "",
},
structure: {
image: "",
card: "",
title: "",
description: "",
},
},
compoundVariants: [
// Style for Basic Variants types
{
variant: "basic",
structure: "image",
className: "w-10 h-auto",
},
{
variant: "basic",
structure: "card",
className: "p-5",
},
{
variant: "basic",
structure: "title",
className: "text-base mt-4",
},
{
variant: "basic",
structure: "description",
className: "text-sm leading-[18px] text-subtle font-normal",
},
// Style for ProfileCard Variant Types
{
variant: "ProfileCard",
structure: "image",
className: "w-9 h-auto rounded-full mb-4s",
},
{
variant: "ProfileCard",
structure: "card",
className: "w-80 p-4 hover:bg-subtle",
},
{
variant: "ProfileCard",
structure: "title",
className: "text-base",
},
{
variant: "ProfileCard",
structure: "description",
className: "text-sm leading-[18px] text-subtle font-normal",
},
// Style for SidebarCard Variant Types
{
variant: "SidebarCard",
structure: "image",
className: "w-9 h-auto rounded-full mb-4s",
},
{
variant: "SidebarCard",
structure: "card",
className: "w-full p-3 border border-subtle",
},
{
variant: "SidebarCard",
structure: "title",
className: "text-sm font-cal",
},
{
variant: "SidebarCard",
structure: "description",
className: "text-xs text-default line-clamp-2",
},
],
});
type CVACardType = Required<Pick<VariantProps<typeof cvaCardTypeByVariant>, "variant">>;
export interface BaseCardProps extends CVACardType {
image?: string; image?: string;
icon?: ReactNode; icon?: ReactNode;
variant: keyof typeof cardTypeByVariant;
imageProps?: JSX.IntrinsicElements["img"]; imageProps?: JSX.IntrinsicElements["img"];
title: string; title: string;
description: ReactNode; description: ReactNode;
@@ -27,30 +115,7 @@ export type BaseCardProps = {
}; };
mediaLink?: string; mediaLink?: string;
thumbnailUrl?: string; thumbnailUrl?: string;
}; }
// @TODO: use CVA
const cardTypeByVariant = {
basic: {
image: "w-10 h-auto",
card: "p-5",
title: "text-base mt-4",
description: "text-sm leading-[18px] text-subtle font-normal",
},
ProfileCard: {
image: "w-9 h-auto rounded-full mb-4s",
card: "w-80 p-4 hover:bg-subtle",
title: "text-base",
description: "text-sm leading-[18px] text-subtle font-normal",
},
SidebarCard: {
image: "w-9 h-auto rounded-full mb-4s",
card: "w-full p-3 border border-subtle",
title: "text-sm font-cal",
description: "text-xs text-default line-clamp-2",
},
};
export function Card({ export function Card({
image, image,
@@ -70,7 +135,7 @@ export function Card({
<div <div
className={classNames( className={classNames(
containerProps?.className, containerProps?.className,
cardTypeByVariant[variant].card, cvaCardTypeByVariant({ variant, structure: "card" }),
"bg-default border-subtle text-default flex flex-col justify-between rounded-md border" "bg-default border-subtle text-default flex flex-col justify-between rounded-md border"
)} )}
{...containerProps}> {...containerProps}>
@@ -81,14 +146,17 @@ export function Card({
src={image} src={image}
// Stops eslint complaining - not smart enough to realise it comes from ...imageProps // Stops eslint complaining - not smart enough to realise it comes from ...imageProps
alt={imageProps?.alt} alt={imageProps?.alt}
className={classNames(imageProps?.className, cardTypeByVariant[variant].image)} className={classNames(
imageProps?.className,
cvaCardTypeByVariant({ variant, structure: "image" })
)}
{...imageProps} {...imageProps}
/> />
)} )}
<h5 <h5
title={title} title={title}
className={classNames( className={classNames(
cardTypeByVariant[variant].title, cvaCardTypeByVariant({ variant, structure: "title" }),
"line-clamp-1 text-emphasis font-bold leading-5" "line-clamp-1 text-emphasis font-bold leading-5"
)}> )}>
{title} {title}
@@ -96,7 +164,7 @@ export function Card({
{description && ( {description && (
<p <p
title={description.toString()} title={description.toString()}
className={classNames(cardTypeByVariant[variant].description, "pt-1")}> className={classNames(cvaCardTypeByVariant({ variant, structure: "description" }), "pt-1")}>
{description} {description}
</p> </p>
)} )}
@@ -0,0 +1,57 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
import {
Examples,
Example,
Note,
Title,
VariantsTable,
VariantColumn,
RowTitles,
CustomArgsTable,
} from "@calcom/storybook/components";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "./sheet";
import {Button} from "../button";
<Meta title="UI/Sheet" />
<Title title="Sheet" suffix="Brief" subtitle="Version 2.0 — Last Update: 21 June 2023" />
## Definition
A sheet is a container that can be used to display content on differnt sides of the current page.
## Structure
<Examples title="Spacing">
<Sheet>
<SheetTrigger asChild>
<Button>Open default sheet</Button>
</SheetTrigger>
<SheetContent position="right" size={"default"}>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose asChild>
<Button type="submit">Save changes</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</Examples>
+207
View File
@@ -0,0 +1,207 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import classNames from "@calcom/lib/classNames";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const portalVariants = cva("fixed inset-0 z-50 flex", {
variants: {
position: {
top: "items-start",
bottom: "items-end",
left: "justify-start",
right: "justify-end",
},
},
defaultVariants: { position: "right" },
});
interface SheetPortalProps extends SheetPrimitive.DialogPortalProps, VariantProps<typeof portalVariants> {}
const SheetPortal = ({ position, className, children, ...props }: SheetPortalProps) => (
<SheetPrimitive.Portal className={classNames(className)} {...props}>
<div className={portalVariants({ position })}>{children}</div>
</SheetPrimitive.Portal>
);
SheetPortal.displayName = SheetPrimitive.Portal.displayName;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<SheetPrimitive.Overlay
className={classNames(
"bg-inverted data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in fixed inset-0 z-50 opacity-60 backdrop-blur-sm transition-all duration-100",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-default m-4 rounded-xl p-6 opacity-100 shadow-lg border",
{
variants: {
position: {
top: "animate-in slide-in-from-top w-full duration-300",
bottom: "animate-in slide-in-from-bottom w-full duration-300",
left: "animate-in slide-in-from-left h-full duration-300",
right: "animate-in slide-in-from-right h-full duration-300",
},
size: {
content: "",
default: "",
sm: "",
lg: "",
xl: "",
full: "",
},
},
compoundVariants: [
{
position: ["top", "bottom"],
size: "content",
class: "max-h-screen",
},
{
position: ["top", "bottom"],
size: "default",
class: "h-1/3",
},
{
position: ["top", "bottom"],
size: "sm",
class: "h-1/4",
},
{
position: ["top", "bottom"],
size: "lg",
class: "h-1/2",
},
{
position: ["top", "bottom"],
size: "xl",
class: "h-5/6",
},
{
position: ["top", "bottom"],
size: "full",
class: "h-screen",
},
{
position: ["right", "left"],
size: "content",
class: "max-w-screen",
},
{
position: ["right", "left"],
size: "default",
class: "w-1/3 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "sm",
class: "w-1/4 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "lg",
class: "w-1/2 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "xl",
class: "w-5/6 h-[calc(100vh-2rem)]",
},
{
position: ["right", "left"],
size: "full",
class: "w-screen h-[calc(100vh-2rem)]",
},
],
defaultVariants: {
position: "right",
size: "default",
},
}
);
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, DialogContentProps>(
({ position, size, className, children, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={classNames(sheetVariants({ position, size }), className)}
{...props}>
{children}
<SheetPrimitive.Close className="focus:ring-emphasis data-[state=open]:bg-deafult absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={classNames("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={classNames("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={classNames("text-foreground text-lg font-semibold", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={classNames("text-muted-foreground text-sm", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
+22 -22
View File
@@ -4062,7 +4062,7 @@ __metadata:
"@calcom/ui": "*" "@calcom/ui": "*"
"@headlessui/react": ^1.5.0 "@headlessui/react": ^1.5.0
"@heroicons/react": ^1.0.6 "@heroicons/react": ^1.0.6
"@prisma/client": ^4.13.0 "@prisma/client": ^4.16.0
"@tailwindcss/forms": ^0.5.2 "@tailwindcss/forms": ^0.5.2
"@types/node": 16.9.1 "@types/node": 16.9.1
"@types/react": 18.0.26 "@types/react": 18.0.26
@@ -4074,7 +4074,7 @@ __metadata:
next-auth: ^4.20.1 next-auth: ^4.20.1
next-i18next: ^11.3.0 next-i18next: ^11.3.0
postcss: ^8.4.18 postcss: ^8.4.18
prisma: ^4.13.0 prisma: ^4.16.0
prisma-field-encryption: ^1.4.0 prisma-field-encryption: ^1.4.0
react: ^18.2.0 react: ^18.2.0
react-chartjs-2: ^4.0.1 react-chartjs-2: ^4.0.1
@@ -4525,10 +4525,10 @@ __metadata:
resolution: "@calcom/prisma@workspace:packages/prisma" resolution: "@calcom/prisma@workspace:packages/prisma"
dependencies: dependencies:
"@calcom/lib": "*" "@calcom/lib": "*"
"@prisma/client": ^4.13.0 "@prisma/client": ^4.16.0
"@prisma/generator-helper": ^4.13.0 "@prisma/generator-helper": ^4.13.0
npm-run-all: ^4.1.5 npm-run-all: ^4.1.5
prisma: ^4.13.0 prisma: ^4.16.0
ts-node: ^10.9.1 ts-node: ^10.9.1
zod: ^3.20.2 zod: ^3.20.2
zod-prisma: ^0.5.4 zod-prisma: ^0.5.4
@@ -7835,17 +7835,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@prisma/client@npm:^4.13.0": "@prisma/client@npm:^4.16.0":
version: 4.13.0 version: 4.16.0
resolution: "@prisma/client@npm:4.13.0" resolution: "@prisma/client@npm:4.16.0"
dependencies: dependencies:
"@prisma/engines-version": 4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a "@prisma/engines-version": 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
peerDependencies: peerDependencies:
prisma: "*" prisma: "*"
peerDependenciesMeta: peerDependenciesMeta:
prisma: prisma:
optional: true optional: true
checksum: 74e2ede6308d6f70dc6244c34d13cf5f6c8da4f1a1ffe42a26192675ecac02733cb5482518d2a37fa057f160873fe8d42f5e3a08fc45d52b1eb386522bdbec0b checksum: 67fbf8e0bd672d597433b61b65a39c58cbb2eb82e7bcac33f31215d8ca5ba48df790a20aa990c47d6dde53e0d84d1a45d07d872f772d3dd8271613abe879b5d0
languageName: node languageName: node
linkType: hard linkType: hard
@@ -7882,17 +7882,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@prisma/engines-version@npm:4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a": "@prisma/engines-version@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c":
version: 4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a version: 4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c
resolution: "@prisma/engines-version@npm:4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" resolution: "@prisma/engines-version@npm:4.16.0-66.b20ead4d3ab9e78ac112966e242ded703f4a052c"
checksum: cc583fe956b65fd4548082917f14ccf1c3c54d6b26623583f5e63298a8b4b5a492ba90597123af1fc272115621cb7340e2d708d5b5e252f59139b35f0df00abe checksum: 23183cab33f95e0711cd6cee7642f4beeeb77b8b0aa54b419f55097bfb96863a11a28f6509ab27fef0ea794522e188044ce850993a25c341333f8305711a255f
languageName: node languageName: node
linkType: hard linkType: hard
"@prisma/engines@npm:4.13.0": "@prisma/engines@npm:4.16.0":
version: 4.13.0 version: 4.16.0
resolution: "@prisma/engines@npm:4.13.0" resolution: "@prisma/engines@npm:4.16.0"
checksum: 6c2315dc1e1a8a07f02a074ee6898a66a96a3bbbb4341224cb1dc966da3959c2ee7557131a2ce37fd559c53d210b904b72c4559474e017438e6753d480c3cfe1 checksum: 60ef1c3720720cf5a9d23f52c668669736716afd1862c4ac61e8ebcd3ca896e3453f80dfab77f3b68b001c5d499a895f28e54cc5025eae61878b40c1ec5d99c3
languageName: node languageName: node
linkType: hard linkType: hard
@@ -29665,15 +29665,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prisma@npm:^4.13.0": "prisma@npm:^4.16.0":
version: 4.13.0 version: 4.16.0
resolution: "prisma@npm:4.13.0" resolution: "prisma@npm:4.16.0"
dependencies: dependencies:
"@prisma/engines": 4.13.0 "@prisma/engines": 4.16.0
bin: bin:
prisma: build/index.js prisma: build/index.js
prisma2: build/index.js prisma2: build/index.js
checksum: b735dd19842da1dfe5b3411551665db5f2a419ee5e67136dfc2c8cae3c7fe0f885b472b0f9decb3adb4af83260c51a605ecca8f2194805804e020c382096a75a checksum: 831a4d14a48474fa144ae0648ed5e3766756b991f1997ecca813a044ef9bd7f2a5794c603adc9d23d867d8f4a0f51cedadaebfd8a841e6923ef9b804ed8bcbbb
languageName: node languageName: node
linkType: hard linkType: hard