Compare commits
49 Commits
main
...
monorepo/a
Author | SHA1 | Date | |
---|---|---|---|
|
5ba094964d | ||
|
26db39f98b | ||
|
26e5904d00 | ||
|
d21d53000e | ||
|
67564fdb35 | ||
|
c9a3c5789e | ||
|
ee95795e0f | ||
|
a369ba895f | ||
|
f0602df29f | ||
|
4f6612f48c | ||
|
10dde1bbf3 | ||
|
35ffa78e92 | ||
|
fd5fd1f9d5 | ||
|
b3f435ec48 | ||
|
ff1e738cc1 | ||
|
579a3af844 | ||
|
9c67a7452c | ||
|
5519cf36e3 | ||
|
f0438ed35d | ||
|
3643d62ce6 | ||
|
93a6039c8c | ||
|
e5c76d10c9 | ||
|
5a63a78660 | ||
|
5c67a95028 | ||
|
505c517237 | ||
|
e5164fc087 | ||
|
12e5c5bf41 | ||
|
c32c29a624 | ||
|
3522af1a16 | ||
|
a7523a7d5d | ||
|
8b2fd0d626 | ||
|
5d2fbe4629 | ||
|
73381dbb93 | ||
|
fa87d34a56 | ||
|
f4f7024d41 | ||
|
355737a86f | ||
|
ed40b09430 | ||
|
c45da4e3fd | ||
|
66cdc01d0b | ||
|
72eb31276c | ||
|
b2bc6500a3 | ||
|
487a4153e4 | ||
|
bab6fcc4ed | ||
|
467d9ad9fd | ||
|
9fcb82cc80 | ||
|
6a7fa041f3 | ||
|
f1da729e1a | ||
|
010d82ec00 | ||
|
801e4c4600 |
|
@ -0,0 +1,229 @@
|
|||
import {
|
||||
BookOpenIcon,
|
||||
DocumentTextIcon,
|
||||
ExternalLinkIcon,
|
||||
FlagIcon,
|
||||
MailIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
//import NavTabs from "@components/NavTabs";
|
||||
import Shell from "@components/Shell";
|
||||
import Badge from "@components/ui/Badge";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export default function App({
|
||||
name,
|
||||
logo,
|
||||
body,
|
||||
categories,
|
||||
author,
|
||||
price,
|
||||
commission,
|
||||
type,
|
||||
docs,
|
||||
website,
|
||||
email,
|
||||
tos,
|
||||
privacy,
|
||||
}: {
|
||||
name: string;
|
||||
logo: string;
|
||||
body: React.ReactNode;
|
||||
categories: string[];
|
||||
author: string;
|
||||
pro?: boolean;
|
||||
price: number;
|
||||
commission?: number;
|
||||
type?: "monthly" | "usage-based" | "one-time" | "free";
|
||||
docs?: string;
|
||||
website?: string;
|
||||
email: string; // required
|
||||
tos?: string;
|
||||
privacy?: string;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
||||
/*const tabs = [
|
||||
{
|
||||
name: t("description"),
|
||||
href: "?description",
|
||||
},
|
||||
{
|
||||
name: t("features"),
|
||||
href: "?features",
|
||||
},
|
||||
{
|
||||
name: t("permissions"),
|
||||
href: "?permissions",
|
||||
},
|
||||
];*/
|
||||
|
||||
const priceInDollar = Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Shell large>
|
||||
<div className="-mx-8">
|
||||
<div className="bg-gray-50 px-10">
|
||||
<Link href="/apps">
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
||||
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center justify-between py-8">
|
||||
<div className="flex">
|
||||
<img className="h-16 w-16" src={logo} alt="" />
|
||||
<header className="px-4 py-2">
|
||||
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
||||
<h2 className="text-sm text-gray-500">
|
||||
<span className="capitalize">{categories[0]}</span> • {t("build_by", { author })}
|
||||
</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
{type === "free" && (
|
||||
<Button onClick={() => alert("TODO: installed free app")}>{t("install_app")}</Button>
|
||||
)}
|
||||
|
||||
{type === "usage-based" && (
|
||||
<Button onClick={() => alert("TODO: installed usage based app")}>{t("install_app")}</Button>
|
||||
)}
|
||||
|
||||
{type === "monthly" && (
|
||||
<Button onClick={() => alert("TODO: installed monthly billed app")}>
|
||||
{t("subscribe")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{price !== 0 && (
|
||||
<small className="block text-right">
|
||||
{type === "usage-based"
|
||||
? commission + "% + " + priceInDollar + "/booking"
|
||||
: priceInDollar}
|
||||
{type === "monthly" && "/" + t("month")}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* reintroduce once we show permissions and features
|
||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between px-10 py-10">
|
||||
<div className="prose-sm prose">{body}</div>
|
||||
<div className="max-w-80 flex-1">
|
||||
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
|
||||
<div className="space-x-2">
|
||||
{categories.map((category) => (
|
||||
<Link href={"/apps/categories/" + category} key={category}>
|
||||
<a>
|
||||
<Badge variant="success">{category}</Badge>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
|
||||
<small>
|
||||
{price === 0 ? (
|
||||
"Free"
|
||||
) : (
|
||||
<>
|
||||
{Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price)}
|
||||
{type === "monthly" && "/" + t("month")}
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
|
||||
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
|
||||
{docs && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={docs}>
|
||||
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{website && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={website}>
|
||||
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{email}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{tos && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={tos}>
|
||||
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{privacy && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<hr className="my-6" />
|
||||
<small className="leading-1 block text-gray-500">
|
||||
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
|
||||
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
|
||||
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
|
||||
</small>
|
||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
||||
<FlagIcon className="inline h-3 w-3" /> Report App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
|
||||
export default function AppsShell({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useLocale();
|
||||
const { status } = useSession();
|
||||
const tabs = [
|
||||
{
|
||||
name: t("app_store"),
|
||||
href: "/apps",
|
||||
},
|
||||
{
|
||||
name: t("installed_apps"),
|
||||
href: "/apps/installed",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-12 block lg:hidden">
|
||||
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
|
||||
</div>
|
||||
<main>{children}</main>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -67,7 +67,7 @@ const DestinationCalendarSelector = ({
|
|||
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
className="focus:border-primary-500 focus:ring-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
|
||||
className="focus:ring-primary-500 focus:border-primary-500 mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-md border-gray-300 sm:text-sm"
|
||||
onChange={(option) => {
|
||||
setSelectedOption(option);
|
||||
if (!option) {
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import { SelectorIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ArrowLeftIcon,
|
||||
ClockIcon,
|
||||
CogIcon,
|
||||
ExternalLinkIcon,
|
||||
LinkIcon,
|
||||
LogoutIcon,
|
||||
PuzzleIcon,
|
||||
ViewGridIcon,
|
||||
MoonIcon,
|
||||
MapIcon,
|
||||
ArrowLeftIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { ReactNode, useEffect, useState } from "react";
|
||||
import React, { Fragment, ReactNode, useEffect, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import LicenseBanner from "@ee/components/LicenseBanner";
|
||||
|
@ -58,6 +58,10 @@ function useRedirectToLoginIfUnauthenticated() {
|
|||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (router.pathname.startsWith("/apps")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!loading && !session) {
|
||||
router.replace({
|
||||
pathname: "/auth/login",
|
||||
|
@ -120,10 +124,11 @@ export function ShellSubHeading(props: {
|
|||
export default function Shell(props: {
|
||||
centered?: boolean;
|
||||
title?: string;
|
||||
heading: ReactNode;
|
||||
heading?: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
children: ReactNode;
|
||||
CTA?: ReactNode;
|
||||
large?: boolean;
|
||||
HeadingLeftIcon?: ReactNode;
|
||||
backPath?: string; // renders back button to specified path
|
||||
// use when content needs to expand with flex
|
||||
|
@ -156,10 +161,22 @@ export default function Shell(props: {
|
|||
current: router.asPath.startsWith("/availability"),
|
||||
},
|
||||
{
|
||||
name: t("integrations"),
|
||||
href: "/integrations",
|
||||
icon: PuzzleIcon,
|
||||
current: router.asPath.startsWith("/integrations"),
|
||||
name: t("apps"),
|
||||
href: "/apps",
|
||||
icon: ViewGridIcon,
|
||||
current: router.asPath.startsWith("/apps"),
|
||||
child: [
|
||||
{
|
||||
name: t("app_store"),
|
||||
href: "/apps",
|
||||
current: router.asPath === "/apps",
|
||||
},
|
||||
{
|
||||
name: t("installed_apps"),
|
||||
href: "/apps/installed",
|
||||
current: router.asPath === "/apps/installed",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: t("settings"),
|
||||
|
@ -181,6 +198,7 @@ export default function Shell(props: {
|
|||
const user = query.data;
|
||||
|
||||
const i18n = useViewerI18n();
|
||||
const { status } = useSession();
|
||||
|
||||
if (i18n.status === "loading" || isRedirectingToOnboarding || loading) {
|
||||
// show spinner whilst i18n is loading to avoid language flicker
|
||||
|
@ -205,59 +223,82 @@ export default function Shell(props: {
|
|||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
|
||||
<div className="flex h-screen overflow-hidden bg-gray-100" data-testid="dashboard-shell">
|
||||
<div className="hidden md:flex lg:flex-shrink-0">
|
||||
<div className="flex w-14 flex-col lg:w-56">
|
||||
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
|
||||
<Link href="/event-types">
|
||||
<a className="px-4 md:hidden lg:inline">
|
||||
<Logo small />
|
||||
</a>
|
||||
</Link>
|
||||
{/* logo icon for tablet */}
|
||||
<Link href="/event-types">
|
||||
<a className="md:inline lg:hidden">
|
||||
<Logo small icon />
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
|
||||
{navigation.map((item) => (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-neutral-100 text-neutral-900"
|
||||
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
|
||||
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
|
||||
)}>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-neutral-500"
|
||||
: "text-neutral-400 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="hidden lg:inline">{item.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<TrialBanner />
|
||||
<div className="m-2 rounded-sm p-2 pt-2 pr-2 hover:bg-gray-100">
|
||||
<span className="hidden lg:inline">
|
||||
<UserDropdown />
|
||||
</span>
|
||||
<span className="hidden md:inline lg:hidden">
|
||||
<UserDropdown small />
|
||||
</span>
|
||||
<div
|
||||
className={classNames("flex h-screen overflow-hidden", props.large ? "bg-white" : "bg-gray-100")}
|
||||
data-testid="dashboard-shell">
|
||||
{status === "authenticated" && (
|
||||
<div className="hidden md:flex lg:flex-shrink-0">
|
||||
<div className="flex w-14 flex-col lg:w-56">
|
||||
<div className="flex h-0 flex-1 flex-col border-r border-gray-200 bg-white">
|
||||
<div className="flex flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
|
||||
<Link href="/event-types">
|
||||
<a className="px-4 md:hidden lg:inline">
|
||||
<Logo small />
|
||||
</a>
|
||||
</Link>
|
||||
{/* logo icon for tablet */}
|
||||
<Link href="/event-types">
|
||||
<a className="md:inline lg:hidden">
|
||||
<Logo small icon />
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="mt-2 flex-1 space-y-1 bg-white px-2 lg:mt-5">
|
||||
{navigation.map((item) => (
|
||||
<Fragment key={item.name}>
|
||||
<Link href={item.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-neutral-100 text-neutral-900"
|
||||
: "text-neutral-500 hover:bg-gray-50 hover:text-neutral-900",
|
||||
"group flex items-center rounded-sm px-2 py-2 text-sm font-medium"
|
||||
)}>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-neutral-500"
|
||||
: "text-neutral-400 group-hover:text-neutral-500",
|
||||
"h-5 w-5 flex-shrink-0 ltr:mr-3 rtl:ml-3"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="hidden lg:inline">{item.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
{item.child &&
|
||||
router.asPath.startsWith(item.href) &&
|
||||
item.child.map((item) => {
|
||||
return (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-neutral-900"
|
||||
: "text-neutral-500 hover:text-neutral-900",
|
||||
"group hidden items-center rounded-sm px-2 py-2 pl-10 text-sm font-medium lg:flex"
|
||||
)}>
|
||||
<span className="hidden lg:inline">{item.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<TrialBanner />
|
||||
<div className="m-2 rounded-sm p-2 pt-2 pr-2 hover:bg-gray-100">
|
||||
<span className="hidden lg:inline">
|
||||
<UserDropdown />
|
||||
</span>
|
||||
<span className="hidden md:inline lg:hidden">
|
||||
<UserDropdown small />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex w-0 flex-1 flex-col overflow-hidden">
|
||||
<main
|
||||
|
@ -266,29 +307,31 @@ export default function Shell(props: {
|
|||
props.flexChildrenContainer && "flex flex-col"
|
||||
)}>
|
||||
{/* show top navigation for md and smaller (tablet and phones) */}
|
||||
<nav className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
|
||||
<Link href="/event-types">
|
||||
<a>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 self-center">
|
||||
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||
<span className="sr-only">{t("view_notifications")}</span>
|
||||
<Link href="/settings/profile">
|
||||
<a>
|
||||
<CogIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</a>
|
||||
</Link>
|
||||
</button>
|
||||
<UserDropdown small />
|
||||
</div>
|
||||
</nav>
|
||||
{status === "authenticated" && (
|
||||
<nav className="flex items-center justify-between border-b border-gray-200 bg-white p-4 md:hidden">
|
||||
<Link href="/event-types">
|
||||
<a>
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 self-center">
|
||||
<button className="rounded-full bg-white p-2 text-gray-400 hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||
<span className="sr-only">{t("view_notifications")}</span>
|
||||
<Link href="/settings/profile">
|
||||
<a>
|
||||
<CogIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</a>
|
||||
</Link>
|
||||
</button>
|
||||
<UserDropdown small />
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
props.centered && "mx-auto md:max-w-5xl",
|
||||
props.flexChildrenContainer && "flex flex-1 flex-col",
|
||||
"py-8"
|
||||
!props.large && "py-8"
|
||||
)}>
|
||||
{!!props.backPath && (
|
||||
<div className="mx-3 mb-8 sm:mx-8">
|
||||
|
@ -300,16 +343,22 @@ export default function Shell(props: {
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8">
|
||||
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
||||
<div className="mb-8 w-full">
|
||||
<h1 className="font-cal mb-1 text-xl font-bold tracking-wide text-gray-900">
|
||||
{props.heading}
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
|
||||
{props.heading && props.subtitle && (
|
||||
<div
|
||||
className={classNames(
|
||||
props.large && "bg-gray-100 py-8 lg:mb-8 lg:pt-16 lg:pb-7",
|
||||
"block min-h-[80px] justify-between px-4 sm:flex sm:px-6 md:px-8"
|
||||
)}>
|
||||
{props.HeadingLeftIcon && <div className="ltr:mr-4">{props.HeadingLeftIcon}</div>}
|
||||
<div className="mb-8 w-full">
|
||||
<h1 className="font-cal mb-1 text-xl font-bold tracking-wide text-gray-900">
|
||||
{props.heading}
|
||||
</h1>
|
||||
<p className="text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{props.subtitle}</p>
|
||||
</div>
|
||||
{props.CTA && <div className="mb-4 flex-shrink-0">{props.CTA}</div>}
|
||||
</div>
|
||||
<div className="mb-4 flex-shrink-0">{props.CTA}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"px-4 sm:px-6 md:px-8",
|
||||
|
@ -318,34 +367,36 @@ export default function Shell(props: {
|
|||
{props.children}
|
||||
</div>
|
||||
{/* show bottom navigation for md and smaller (tablet and phones) */}
|
||||
<nav className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
|
||||
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
|
||||
{navigation.flatMap((item, itemIdx) =>
|
||||
item.href === "/settings/profile" ? (
|
||||
[]
|
||||
) : (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
|
||||
itemIdx === 0 ? "rounded-l-lg" : "",
|
||||
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
|
||||
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}>
|
||||
<item.icon
|
||||
{status === "authenticated" && (
|
||||
<nav className="bottom-nav fixed bottom-0 z-30 flex w-full bg-white shadow md:hidden">
|
||||
{/* note(PeerRich): using flatMap instead of map to remove settings from bottom nav */}
|
||||
{navigation.flatMap((item, itemIdx) =>
|
||||
item.href === "/settings/profile" ? (
|
||||
[]
|
||||
) : (
|
||||
<Link key={item.name} href={item.href}>
|
||||
<a
|
||||
className={classNames(
|
||||
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
||||
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
|
||||
item.current ? "text-gray-900" : "text-neutral-400 hover:text-gray-700",
|
||||
itemIdx === 0 ? "rounded-l-lg" : "",
|
||||
itemIdx === navigation.length - 1 ? "rounded-r-lg" : "",
|
||||
"group relative min-w-0 flex-1 overflow-hidden bg-white py-2 px-2 text-center text-xs font-medium hover:bg-gray-50 focus:z-10 sm:text-sm"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
aria-current={item.current ? "page" : undefined}>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current ? "text-gray-900" : "text-gray-400 group-hover:text-gray-500",
|
||||
"mx-auto mb-1 block h-5 w-5 flex-shrink-0 text-center"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
{/* add padding to content for mobile navigation*/}
|
||||
<div className="block pt-12 md:hidden" />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
|
||||
export default function AllApps({ apps }: { apps: App[] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
||||
<div className="grid-col-1 grid gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
name={app.name}
|
||||
slug={app.slug}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { StarIcon } from "@heroicons/react/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
interface AppCardProps {
|
||||
logo: string;
|
||||
name: string;
|
||||
slug?: string;
|
||||
category?: string;
|
||||
description: string;
|
||||
rating: number;
|
||||
reviews?: number;
|
||||
}
|
||||
|
||||
export default function AppCard(props: AppCardProps) {
|
||||
return (
|
||||
<Link href={"/apps/" + props.slug}>
|
||||
<a className="block rounded-sm border border-gray-300 p-5 hover:bg-neutral-50">
|
||||
<div className="flex">
|
||||
<img src={props.logo} alt={props.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
<Button
|
||||
color="secondary"
|
||||
className="ml-auto flex self-start"
|
||||
onClick={() => {
|
||||
// TODO: Actually add the integration
|
||||
console.log("The magic is supposed to happen here");
|
||||
}}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<h3 className="font-medium">{props.name}</h3>
|
||||
<div className="flex text-sm text-gray-800">
|
||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500">{props.description}</p>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { CreditCardIcon } from "@heroicons/react/outline";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
export default function AppStoreCategories(props: any) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{props.categories.map((category: any) => (
|
||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
||||
<a className="flex rounded-sm bg-gray-100 px-6 py-4">
|
||||
<div className="mr-4 flex h-12 w-12 rounded-sm bg-white">
|
||||
<CreditCardIcon className="mx-auto h-6 w-6 self-center" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">{category.count} apps</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import Glide from "@glidejs/glide";
|
||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
||||
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useMediaQuery from "@lib/hooks/useMediaQuery";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
|
||||
const Slider = <T extends App>({ items }: { items: T[] }) => {
|
||||
const { t } = useLocale();
|
||||
const isMobile = useMediaQuery("(max-width: 767px)");
|
||||
const [size, setSize] = useState(3);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
setSize(1);
|
||||
} else {
|
||||
setSize(3);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
new Glide(".glide", {
|
||||
type: "carousel",
|
||||
perView: size,
|
||||
}).mount();
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<div className="glide">
|
||||
<div className="flex cursor-default">
|
||||
<div>
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("trending_apps")}</h2>
|
||||
</div>
|
||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
||||
<button data-glide-dir="<" className="mr-4">
|
||||
<ArrowLeftIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
<button data-glide-dir=">">
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="glide__track" data-glide-el="track">
|
||||
<ul className="glide__slides">
|
||||
{items.map((app) => {
|
||||
return (
|
||||
app.trending && (
|
||||
<li key={app.name} className="glide__slide">
|
||||
<AppCard
|
||||
key={app.name}
|
||||
name={app.name}
|
||||
slug={app.slug}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
|
@ -141,6 +141,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
[LocationType.Daily]: "Daily.co Video",
|
||||
[LocationType.Huddle01]: "Huddle01 Video",
|
||||
[LocationType.Tandem]: "Tandem Video",
|
||||
[LocationType.Teams]: "MS Teams",
|
||||
};
|
||||
|
||||
const defaultValues = () => {
|
||||
|
|
|
@ -3,11 +3,12 @@ import { useState } from "react";
|
|||
import { useMutation } from "react-query";
|
||||
|
||||
import { NEXT_PUBLIC_BASE_URL } from "@lib/config/constants";
|
||||
import { AddAppleIntegrationModal } from "@lib/integrations/calendar/components/AddAppleIntegration";
|
||||
import { AddCalDavIntegrationModal } from "@lib/integrations/calendar/components/AddCalDavIntegration";
|
||||
|
||||
import { ButtonBaseProps } from "@components/ui/Button";
|
||||
|
||||
import { AddIntegrationModal as AddAppleIntegrationModal } from "../../lib/apps/apple_calendar/components/AddIntegration";
|
||||
import { AddIntegrationModal as AddCalDavIntegrationModal } from "../../lib/apps/caldav_calendar/components/AddIntegration";
|
||||
|
||||
export default function ConnectIntegration(props: {
|
||||
type: string;
|
||||
render: (renderProps: ButtonBaseProps) => JSX.Element;
|
||||
|
|
|
@ -6,7 +6,7 @@ import classNames from "@lib/classNames";
|
|||
import { ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||
|
||||
function IntegrationListItem(props: {
|
||||
imageSrc: string;
|
||||
imageSrc?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
|
@ -15,7 +15,7 @@ function IntegrationListItem(props: {
|
|||
return (
|
||||
<ListItem expanded={!!props.children} className={classNames("flex-col")}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
||||
<Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />
|
||||
{props.imageSrc && <Image width={40} height={40} src={`/${props.imageSrc}`} alt={props.title} />}
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3">{props.title}</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import { MembershipRole } from "@prisma/client";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
interface Props {
|
||||
role?: MembershipRole;
|
||||
invitePending?: boolean;
|
||||
}
|
||||
|
||||
export default function TeamRole(props: Props) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
"self-center rounded-md border px-3 py-1 text-xs capitalize ltr:mr-2 rtl:ml-2",
|
||||
{
|
||||
"border-blue-200 bg-blue-50 text-blue-700": props.role === "MEMBER",
|
||||
"border-gray-200 bg-gray-50 text-gray-700": props.role === "OWNER",
|
||||
"border-red-200 bg-red-50 text-red-700": props.role === "ADMIN",
|
||||
"border-yellow-200 bg-yellow-50 text-yellow-700": props.invitePending,
|
||||
}
|
||||
)}>
|
||||
{(() => {
|
||||
if (props.invitePending) return t("invitee");
|
||||
switch (props.role) {
|
||||
case "OWNER":
|
||||
return t("owner");
|
||||
case "ADMIN":
|
||||
return t("admin");
|
||||
case "MEMBER":
|
||||
return t("member");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})()}
|
||||
</span>
|
||||
);
|
||||
}
|
|
@ -10,8 +10,8 @@ export default function LinkIconButton(props: LinkIconButtonProps) {
|
|||
return (
|
||||
<div className="-ml-2">
|
||||
<button
|
||||
type="button"
|
||||
{...props}
|
||||
type="button"
|
||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||
<props.Icon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
{props.children}
|
||||
|
|
|
@ -16,7 +16,7 @@ export const DatePicker = ({ date, onDatesChange, className }: Props) => {
|
|||
return (
|
||||
<PrimitiveDatePicker
|
||||
className={classNames(
|
||||
"focus:border-primary-500 focus:ring-primary-500 rounded-sm border border-gray-300 p-1 pl-2 shadow-sm sm:text-sm",
|
||||
"focus:ring-primary-500 focus:border-primary-500 rounded-sm border border-gray-300 p-1 pl-2 shadow-sm sm:text-sm",
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
|
|
|
@ -21,7 +21,7 @@ const TrialBanner = () => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="m-4 hidden rounded-md bg-yellow-200 p-4 text-center text-sm font-medium text-gray-600 lg:block"
|
||||
className="m-4 hidden rounded-md bg-yellow-200 p-4 text-center text-sm font-medium text-gray-600 sm:block"
|
||||
data-testid="trial-banner">
|
||||
<div className="mb-2 text-left">{t("trial_days_left", { days: trialDaysLeft })}</div>
|
||||
<Button
|
||||
|
|
|
@ -74,7 +74,7 @@ export default function TeamAvailabilityModal(props: Props) {
|
|||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
<Avatar
|
||||
imageSrc={getPlaceholderAvatar(member?.avatar, member?.name as string)}
|
||||
alt={member?.name || ""}
|
||||
className="min-h-10 min-w-10 mt-1 h-10 w-10 rounded-full"
|
||||
className="min-w-10 min-h-10 mt-1 h-10 w-10 rounded-full"
|
||||
/>
|
||||
<div className="ml-3 inline-block overflow-hidden pt-1">
|
||||
<span className="truncate text-lg font-bold text-neutral-700">{member?.name}</span>
|
||||
|
@ -93,7 +93,7 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="react-select-container focus:border-primary-500 focus:ring-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
className="react-select-container focus:ring-primary-500 focus:border-primary-500 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
|
|
|
@ -59,7 +59,7 @@ export default function TeamAvailabilityTimes(props: Props) {
|
|||
{times.map((time) => (
|
||||
<div key={time.format()} className="flex flex-row items-center">
|
||||
<a
|
||||
className="min-w-48 border-brand text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 mr-3 block flex-grow rounded-sm border bg-white py-2 text-center font-medium dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black dark:hover:bg-black dark:hover:text-white"
|
||||
className="min-w-48 border-brand text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast dark:text-neutral-200 mb-2 mr-3 block flex-grow rounded-sm border bg-white py-2 text-center font-medium dark:border-transparent dark:bg-gray-600 dark:hover:border-black dark:hover:bg-black dark:hover:text-white"
|
||||
data-testid="time">
|
||||
{time.format("HH:mm")}
|
||||
</a>
|
||||
|
|
|
@ -94,7 +94,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
|
|||
const verifyButton = useMemo(() => {
|
||||
return (
|
||||
<Button color="secondary" onClick={verifyWallet} type="button" id="hasToken" name="hasToken">
|
||||
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
|
||||
<img className="mr-1 h-5" src="/apps/metamask.svg" />
|
||||
{t("verify_wallet")}
|
||||
</Button>
|
||||
);
|
||||
|
@ -103,7 +103,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
|
|||
const connectButton = useMemo(() => {
|
||||
return (
|
||||
<Button color="secondary" onClick={connectMetamask} type="button">
|
||||
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
|
||||
<img className="mr-1 h-5" src="/apps/metamask.svg" />
|
||||
{t("connect_metamask")}
|
||||
</Button>
|
||||
);
|
||||
|
@ -118,7 +118,7 @@ const CryptoSection = (props: CryptoSectionProps) => {
|
|||
await connectMetamask();
|
||||
await verifyWallet();
|
||||
}}>
|
||||
<img className="mr-1 h-5" src="/integrations/metamask.svg" />
|
||||
<img className="mr-1 h-5" src="/apps/metamask.svg" />
|
||||
{t("verify_wallet")}
|
||||
</Button>
|
||||
);
|
||||
|
|
|
@ -3,10 +3,10 @@ import Stripe from "stripe";
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
import { createPaymentLink } from "./client";
|
||||
|
||||
|
|
|
@ -2,13 +2,13 @@ import { buffer } from "micro";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import stripe from "@ee/lib/stripe/server";
|
||||
|
||||
import { IS_PRODUCTION } from "@lib/config/constants";
|
||||
import { HttpError as HttpCode } from "@lib/core/http/error";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
|
|
@ -15,6 +15,7 @@ const config: Config.InitialOptions = {
|
|||
"^@lib(.*)$": "<rootDir>/lib$1",
|
||||
"^@server(.*)$": "<rootDir>/server$1",
|
||||
"^@ee(.*)$": "<rootDir>/ee$1",
|
||||
"^@apps(.*)$": "<rootDir>/lib/apps$1",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ import { Person } from "ics";
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getAppName } from "@lib/apps/utils/AppUtils";
|
||||
|
||||
import { BASE_URL } from "./config/constants";
|
||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||
|
||||
const translator = short();
|
||||
|
||||
|
@ -54,7 +55,7 @@ ${calEvent.description}
|
|||
};
|
||||
|
||||
export const getLocation = (calEvent: CalendarEvent) => {
|
||||
let providerName = calEvent.location ? getIntegrationName(calEvent.location) : "";
|
||||
let providerName = calEvent.location ? getAppName(calEvent.location) : "";
|
||||
|
||||
if (calEvent.location && calEvent.location.includes("integrations:")) {
|
||||
const location = calEvent.location.split(":")[1];
|
||||
|
|
|
@ -13,9 +13,9 @@ import { Form, TextField } from "@components/form/fields";
|
|||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export const ADD_APPLE_INTEGRATION_FORM_TITLE = "addAppleIntegration";
|
||||
export const ADD_INTEGRATION_FORM_TITLE = "addAppleIntegration";
|
||||
|
||||
export function AddAppleIntegrationModal(props: DialogProps) {
|
||||
export function AddIntegrationModal(props: DialogProps) {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
username: "",
|
|
@ -0,0 +1 @@
|
|||
export const APPLE_CALENDAR_URL = "https://caldav.icloud.com";
|
|
@ -0,0 +1,11 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { APPS_TYPES } from "../../calendar/constants/general";
|
||||
import CalendarService from "../../calendar/services/CalendarService";
|
||||
import { APPLE_CALENDAR_URL } from "../constants/general";
|
||||
|
||||
export default class AppleCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, APPS_TYPES.apple, APPLE_CALENDAR_URL);
|
||||
}
|
||||
}
|
|
@ -17,14 +17,15 @@ type Props = {
|
|||
onSubmit: () => void;
|
||||
};
|
||||
|
||||
export const ADD_CALDAV_INTEGRATION_FORM_TITLE = "addCalDav";
|
||||
export type AddCalDavIntegrationRequest = {
|
||||
export const ADD_INTEGRATION_FORM_TITLE = "addCalDav";
|
||||
|
||||
export type AddIntegrationRequest = {
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function AddCalDavIntegrationModal(props: DialogProps) {
|
||||
export function AddIntegrationModal(props: DialogProps) {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
url: "",
|
||||
|
@ -118,7 +119,7 @@ const AddCalDavIntegration = React.forwardRef<HTMLFormElement, Props>((props, re
|
|||
};
|
||||
|
||||
return (
|
||||
<form id={ADD_CALDAV_INTEGRATION_FORM_TITLE} ref={ref} onSubmit={onSubmit}>
|
||||
<form id={ADD_INTEGRATION_FORM_TITLE} ref={ref} onSubmit={onSubmit}>
|
||||
<div className="mb-2">
|
||||
<label htmlFor="url" className="block text-sm font-medium text-gray-700">
|
||||
Calendar URL
|
|
@ -0,0 +1,10 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { APPS_TYPES } from "../../calendar/constants/general";
|
||||
import CalendarService from "../../calendar/services/CalendarService";
|
||||
|
||||
export default class CalDavCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, APPS_TYPES.caldav);
|
||||
}
|
||||
}
|
|
@ -9,15 +9,14 @@ import { trpc } from "@lib/trpc";
|
|||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||
import { List } from "@components/List";
|
||||
import { ShellSubHeading } from "@components/Shell";
|
||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
import Switch from "@components/ui/Switch";
|
||||
|
||||
import ConnectIntegration from "./ConnectIntegrations";
|
||||
import DisconnectIntegration from "./DisconnectIntegration";
|
||||
import IntegrationListItem from "./IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "./SubHeadingTitleWithConnections";
|
||||
|
||||
type Props = {
|
||||
onChanged: () => unknown | Promise<unknown>;
|
||||
};
|
||||
|
@ -126,8 +125,8 @@ function ConnectedCalendarsList(props: Props) {
|
|||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId as string}
|
||||
title={cal.name as string}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
defaultSelected={cal.isSelected}
|
||||
/>
|
||||
|
@ -192,6 +191,7 @@ function CalendarList(props: Props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarListContainer(props: { heading?: false }) {
|
||||
const { t } = useLocale();
|
||||
const { heading = true } = props;
|
|
@ -0,0 +1,42 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import { validJson } from "../../jsonUtils";
|
||||
|
||||
export const APPS = {
|
||||
google_calendar: {
|
||||
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
|
||||
type: "google_calendar",
|
||||
title: "Google Calendar",
|
||||
name: "Google Calendar",
|
||||
description: "For personal and business calendars",
|
||||
imageSrc: "apps/google-calendar.svg",
|
||||
variant: "calendar",
|
||||
},
|
||||
office365_calendar: {
|
||||
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
|
||||
type: "office365_calendar",
|
||||
title: "Office 365 / Outlook.com Calendar",
|
||||
name: "Office 365 Calendar",
|
||||
description: "For personal and business calendars",
|
||||
imageSrc: "apps/outlook.svg",
|
||||
variant: "calendar",
|
||||
},
|
||||
caldav_calendar: {
|
||||
installed: true,
|
||||
type: "caldav_calendar",
|
||||
title: "CalDav Server",
|
||||
name: "CalDav Server",
|
||||
imageSrc: "apps/caldav.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
apple_calendar: {
|
||||
installed: true,
|
||||
type: "apple_calendar",
|
||||
title: "Apple Calendar",
|
||||
name: "Apple Calendar",
|
||||
description: "For personal and business calendars",
|
||||
imageSrc: "apps/apple-calendar.svg",
|
||||
variant: "calendar",
|
||||
},
|
||||
} as Record<string, App>;
|
|
@ -0,0 +1,8 @@
|
|||
export const DEFAULT_CALENDAR_TYPE = "caldav";
|
||||
|
||||
export const APPS_TYPES = {
|
||||
apple: "apple_calendar",
|
||||
caldav: "caldav_calendar",
|
||||
google: "google_calendar",
|
||||
office365: "office365_calendar",
|
||||
};
|
|
@ -1,21 +1,11 @@
|
|||
import { DestinationCalendar, SelectedCalendar } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
import { SelectedCalendar } from "@prisma/client";
|
||||
|
||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
import type { CalendarEvent, ConferenceData } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import type { Event } from "@lib/events/EventManager";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
import { NewCalendarEventType } from "../constants/types";
|
||||
import { ConferenceData } from "./GoogleCalendar";
|
||||
|
||||
export type Person = {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
language: { translate: TFunction; locale: string };
|
||||
};
|
||||
import { NewCalendarEventType } from "../types/CalendarTypes";
|
||||
|
||||
export interface EntryPoint {
|
||||
entryPointType?: string;
|
||||
|
@ -34,29 +24,6 @@ export interface AdditionInformation {
|
|||
hangoutLink?: string;
|
||||
}
|
||||
|
||||
export interface CalendarEvent {
|
||||
type: string;
|
||||
title: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
description?: string | null;
|
||||
team?: {
|
||||
name: string;
|
||||
members: string[];
|
||||
};
|
||||
location?: string | null;
|
||||
organizer: Person;
|
||||
attendees: Person[];
|
||||
conferenceData?: ConferenceData;
|
||||
additionInformation?: AdditionInformation;
|
||||
uid?: string | null;
|
||||
videoCallData?: VideoCallData;
|
||||
paymentInfo?: PaymentInfo | null;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
cancellationReason?: string | null;
|
||||
rejectionReason?: string | null;
|
||||
}
|
||||
|
||||
export interface IntegrationCalendar extends Ensure<Partial<SelectedCalendar>, "externalId"> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
|
@ -1,26 +1,28 @@
|
|||
import { Credential, SelectedCalendar } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { EventResult } from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
|
||||
import { ALL_INTEGRATIONS } from "../getIntegrations";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "./constants/generals";
|
||||
import { CalendarServiceType, EventBusyDate } from "./constants/types";
|
||||
import { Calendar, CalendarEvent } from "./interfaces/Calendar";
|
||||
import AppleCalendarService from "./services/AppleCalendarService";
|
||||
import CalDavCalendarService from "./services/CalDavCalendarService";
|
||||
import GoogleCalendarService from "./services/GoogleCalendarService";
|
||||
import Office365CalendarService from "./services/Office365CalendarService";
|
||||
import AppleCalendarService from "../../apple_calendar/services/CalendarService";
|
||||
import CalDavCalendarService from "../../caldav_calendar/services/CalendarService";
|
||||
import GoogleCalendarService from "../../google_calendar/services/CalendarService";
|
||||
import Office365CalendarService from "../../office365_calendar/services/CalendarService";
|
||||
import { APPS } from "../config";
|
||||
import { APPS_TYPES } from "../constants/general";
|
||||
import { Calendar } from "../interfaces/Calendar";
|
||||
import { CalendarServiceType, EventBusyDate } from "../types/CalendarTypes";
|
||||
|
||||
const CALENDARS: Record<string, CalendarServiceType> = {
|
||||
[CALENDAR_INTEGRATIONS_TYPES.apple]: AppleCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.caldav]: CalDavCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.google]: GoogleCalendarService,
|
||||
[CALENDAR_INTEGRATIONS_TYPES.office365]: Office365CalendarService,
|
||||
[APPS_TYPES.apple]: AppleCalendarService,
|
||||
[APPS_TYPES.caldav]: CalDavCalendarService,
|
||||
[APPS_TYPES.google]: GoogleCalendarService,
|
||||
[APPS_TYPES.office365]: Office365CalendarService,
|
||||
};
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["CalendarManager"] });
|
||||
|
@ -41,7 +43,7 @@ export const getCalendarCredentials = (credentials: Array<Omit<Credential, "user
|
|||
const calendarCredentials = credentials
|
||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
||||
.flatMap((credential) => {
|
||||
const integration = ALL_INTEGRATIONS.find((integration) => integration.type === credential.type);
|
||||
const integration = APPS[credential.type];
|
||||
|
||||
const calendar = getCalendar({
|
||||
...credential,
|
|
@ -16,15 +16,17 @@ import {
|
|||
} from "tsdav";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { symmetricDecrypt } from "@lib/crypto";
|
||||
import type { Event } from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import { TIMEZONE_FORMAT } from "../constants/formats";
|
||||
import { CALDAV_CALENDAR_TYPE } from "../constants/generals";
|
||||
import { CalendarEventType, EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import { TIMEZONE_FORMAT } from "../constants/format";
|
||||
import { DEFAULT_CALENDAR_TYPE } from "../constants/general";
|
||||
import { Calendar, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import { CalendarEventType, EventBusyDate, NewCalendarEventType } from "../types/CalendarTypes";
|
||||
import { convertDate, getAttendees, getDuration } from "../utils/CalendarUtils";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
@ -360,7 +362,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
return createAccount({
|
||||
account: {
|
||||
serverUrl: this.url,
|
||||
accountType: CALDAV_CALENDAR_TYPE,
|
||||
accountType: DEFAULT_CALENDAR_TYPE,
|
||||
credentials: this.credentials,
|
||||
},
|
||||
headers: this.headers,
|
|
@ -1,10 +1,18 @@
|
|||
import dayjs from "dayjs";
|
||||
import ICAL from "ical.js";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import AppleCalendarService from "../services/AppleCalendarService";
|
||||
import CalDavCalendarService from "../services/CalDavCalendarService";
|
||||
import GoogleCalendarService from "../services/GoogleCalendarService";
|
||||
import Office365CalendarService from "../services/Office365CalendarService";
|
||||
import AppleCalendarService from "../../apple_calendar/services/CalendarService";
|
||||
import CalDavCalendarService from "../../apple_calendar/services/CalendarService";
|
||||
import GoogleCalendarService from "../../google_calendar/services/CalendarService";
|
||||
import Office365CalendarService from "../../office365_calendar/services/CalendarService";
|
||||
|
||||
export type Person = {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone: string;
|
||||
language: { translate: TFunction; locale: string };
|
||||
};
|
||||
|
||||
export type EventBusyDate = Record<"start" | "end", Date | string>;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
|
||||
export const APPS = {
|
||||
tandem_video: {
|
||||
installed: !!(process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET),
|
||||
type: "tandem_video",
|
||||
title: "Tandem Video",
|
||||
imageSrc: "integrations/tandem.svg",
|
||||
description: "Virtual Office | Video Conferencing",
|
||||
variant: "conferencing",
|
||||
name: "Daily",
|
||||
label: "",
|
||||
slug: "",
|
||||
category: "",
|
||||
logo: "",
|
||||
publisher: "",
|
||||
url: "",
|
||||
verified: true,
|
||||
trending: true,
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
},
|
||||
} as Record<string, App>;
|
||||
|
||||
export const ALL_INTEGRATIONS = [
|
||||
{
|
||||
installed: true,
|
||||
type: "metamask_web3",
|
||||
title: "Metamask",
|
||||
imageSrc: "integrations/apple-calendar.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "web3",
|
||||
},
|
||||
];
|
|
@ -2,14 +2,16 @@ import { Credential, Prisma } from "@prisma/client";
|
|||
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";
|
||||
import { Auth, calendar_v3, google } from "googleapis";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "@lib/integrations/calendar/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
import { APPS_TYPES } from "../../calendar/constants/general";
|
||||
import { Calendar, IntegrationCalendar } from "../../calendar/interfaces/Calendar";
|
||||
import CalendarService from "../../calendar/services/CalendarService";
|
||||
import { EventBusyDate, NewCalendarEventType } from "../../calendar/types/CalendarTypes";
|
||||
|
||||
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "";
|
||||
|
||||
|
@ -20,7 +22,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||
private log: typeof logger;
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = CALENDAR_INTEGRATIONS_TYPES.google;
|
||||
this.integrationName = APPS_TYPES.google;
|
||||
|
||||
this.auth = this.googleAuth(credential);
|
||||
|
|
@ -1,15 +1,17 @@
|
|||
import { Calendar as OfficeCalendar } from "@microsoft/microsoft-graph-types-beta";
|
||||
import { Credential } from "@prisma/client";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getLocation, getRichDescription } from "@lib/CalEventParser";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "@lib/integrations/calendar/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { BatchResponse, EventBusyDate, NewCalendarEventType } from "../constants/types";
|
||||
import { Calendar, CalendarEvent, IntegrationCalendar } from "../interfaces/Calendar";
|
||||
import { BufferedBusyTime, O365AuthCredentials } from "../interfaces/Office365Calendar";
|
||||
import { APPS_TYPES } from "../../calendar/constants/general";
|
||||
import { Calendar, IntegrationCalendar } from "../../calendar/interfaces/Calendar";
|
||||
import { BatchResponse, EventBusyDate, NewCalendarEventType } from "../../calendar/types/CalendarTypes";
|
||||
import { BufferedBusyTime, O365AuthCredentials } from "../types/Office365Calendar";
|
||||
|
||||
const MS_GRAPH_CLIENT_ID = process.env.MS_GRAPH_CLIENT_ID || "";
|
||||
const MS_GRAPH_CLIENT_SECRET = process.env.MS_GRAPH_CLIENT_SECRET || "";
|
||||
|
@ -21,7 +23,7 @@ export default class Office365CalendarService implements Calendar {
|
|||
auth: { getToken: () => Promise<string> };
|
||||
|
||||
constructor(credential: Credential) {
|
||||
this.integrationName = CALENDAR_INTEGRATIONS_TYPES.office365;
|
||||
this.integrationName = APPS_TYPES.office365;
|
||||
this.auth = this.o365Auth(credential);
|
||||
|
||||
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
|
@ -0,0 +1,17 @@
|
|||
import type { App } from "@calcom/types/App";
|
||||
|
||||
export const APPS = {
|
||||
stripe_payment: {
|
||||
installed: !!(
|
||||
process.env.STRIPE_CLIENT_ID &&
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_PRIVATE_KEY
|
||||
),
|
||||
type: "stripe_payment",
|
||||
title: "Stripe",
|
||||
name: "Stripe",
|
||||
imageSrc: "apps/stripe.svg",
|
||||
description: "Collect payments",
|
||||
variant: "payment",
|
||||
},
|
||||
} as Record<string, App>;
|
|
@ -0,0 +1,111 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import { LocationType } from "@calcom/lib/location";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import { APPS as CalendarApps } from "@lib/apps/calendar/config";
|
||||
import { APPS as ConferencingApps } from "@lib/apps/conferencing/config";
|
||||
import { APPS as PaymentApps } from "@lib/apps/payment/config";
|
||||
|
||||
const ALL_APPS_MAP = {
|
||||
...Object.values(appStore).map((app) => app.metadata),
|
||||
/* To be deprecated start */
|
||||
...CalendarApps,
|
||||
...ConferencingApps,
|
||||
...PaymentApps,
|
||||
/* To be deprecated end */
|
||||
} as App[];
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
|
||||
export const ALL_APPS = Object.values(ALL_APPS_MAP);
|
||||
|
||||
type OptionTypeBase = {
|
||||
label: string;
|
||||
value: LocationType;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function getLocationOptions(integrations: AppMeta) {
|
||||
const defaultLocations: OptionTypeBase[] = [
|
||||
{ value: LocationType.InPerson, label: "in_person_meeting" },
|
||||
{ value: LocationType.Phone, label: "phone_call" },
|
||||
];
|
||||
|
||||
integrations.forEach((app) => {
|
||||
if (app.locationOption) {
|
||||
defaultLocations.push(app.locationOption);
|
||||
}
|
||||
});
|
||||
|
||||
return defaultLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* This should get all avaialable apps to the user based on his saved
|
||||
* credentials, this should also get globally available apps.
|
||||
*/
|
||||
function getApps(userCredentials: CredentialData[]) {
|
||||
const apps = ALL_APPS.map((appMeta) => {
|
||||
const appName = appMeta.type.split("_").join("");
|
||||
const app = appStore[appName as keyof typeof appStore];
|
||||
const credentials = userCredentials
|
||||
.filter((credential) => credential.type === appMeta.type)
|
||||
.map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend
|
||||
let locationOption: OptionTypeBase | null = null;
|
||||
|
||||
/** Check if app has location option AND add it if user has credentials for it OR is a global one */
|
||||
if (app && "lib" in app && "locationOption" in app.lib && (appMeta.isGlobal || credentials.length > 0)) {
|
||||
locationOption = app.lib.locationOption;
|
||||
}
|
||||
|
||||
const credential: typeof credentials[number] | null = credentials[0] || null;
|
||||
return {
|
||||
...appMeta,
|
||||
/**
|
||||
* @deprecated use `credentials`
|
||||
*/
|
||||
credential,
|
||||
credentials,
|
||||
/** Option to display in `location` field while editing event types */
|
||||
locationOption,
|
||||
};
|
||||
});
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export type AppMeta = ReturnType<typeof getApps>;
|
||||
|
||||
/** @deprecated use `getApps` */
|
||||
export function hasIntegration(apps: AppMeta, type: string): boolean {
|
||||
return !!apps.find((app) => app.type === type && !!app.installed && app.credentials.length > 0);
|
||||
}
|
||||
|
||||
export function hasIntegrationInstalled(type: App["type"]): boolean {
|
||||
return ALL_APPS.some((app) => app.type === type && !!app.installed);
|
||||
}
|
||||
|
||||
export function getAppName(name: string) {
|
||||
return ALL_APPS_MAP[name].name;
|
||||
}
|
||||
|
||||
export function getAppType(name: string): string {
|
||||
const type = ALL_APPS_MAP[name].type;
|
||||
|
||||
if (type.endsWith("_calendar")) {
|
||||
return "Calendar";
|
||||
}
|
||||
if (type.endsWith("_payment")) {
|
||||
return "Payment";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
export default getApps;
|
|
@ -1,6 +1,2 @@
|
|||
export const BASE_URL = process.env.BASE_URL || `https://${process.env.VERCEL_URL}`;
|
||||
export const WEBSITE_URL = process.env.NEXT_PUBLIC_APP_URL || "https://cal.com";
|
||||
export const IS_PRODUCTION = process.env.NODE_ENV === "production";
|
||||
export const TRIAL_LIMIT_DAYS = 14;
|
||||
export const HOSTED_CAL_FEATURES = process.env.HOSTED_CAL_FEATURES || BASE_URL === "https://app.cal.com";
|
||||
export const NEXT_PUBLIC_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || `https://${process.env.VERCEL_URL}`;
|
||||
// TODO: Remove this file once everything is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/constants";
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import parser from "accept-language-parser";
|
||||
import { IncomingMessage } from "http";
|
||||
import { i18n } from "next-i18next.config";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { Maybe } from "@trpc/server";
|
||||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
let preferredLocale: string | null | undefined;
|
||||
if (req.headers["accept-language"]) {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import type { Person } from "@lib/apps/calendar/types/CalendarTypes";
|
||||
import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaiting-payment-email";
|
||||
import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email";
|
||||
import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email";
|
||||
|
@ -12,7 +15,6 @@ import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-reque
|
|||
import OrganizerRescheduledEmail from "@lib/emails/templates/organizer-rescheduled-email";
|
||||
import OrganizerScheduledEmail from "@lib/emails/templates/organizer-scheduled-email";
|
||||
import TeamInviteEmail, { TeamInvite } from "@lib/emails/templates/team-invite-email";
|
||||
import { CalendarEvent, Person } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
export const sendScheduledEmails = async (calEvent: CalendarEvent) => {
|
||||
const emailsToSend: Promise<unknown>[] = [];
|
||||
|
|
|
@ -6,10 +6,12 @@ import utc from "dayjs/plugin/utc";
|
|||
import { createEvent, DateArray } from "ics";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { CalendarEvent, Person } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import type { Person } from "@lib/apps/calendar/types/CalendarTypes";
|
||||
import { getAppName } from "@lib/apps/utils/AppUtils";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import {
|
||||
|
@ -308,7 +310,7 @@ ${getRichDescription(this.calEvent)}
|
|||
}
|
||||
|
||||
protected getLocation(): string {
|
||||
let providerName = this.calEvent.location ? getIntegrationName(this.calEvent.location) : "";
|
||||
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
|
||||
|
||||
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
|
||||
const location = this.calEvent.location.split(":")[1];
|
||||
|
|
|
@ -6,10 +6,11 @@ import utc from "dayjs/plugin/utc";
|
|||
import { createEvent, DateArray, Person } from "ics";
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { getCancelLink, getRichDescription } from "@lib/CalEventParser";
|
||||
import { getAppName } from "@lib/apps/utils/AppUtils";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getIntegrationName } from "@lib/integrations";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { serverConfig } from "@lib/serverConfig";
|
||||
|
||||
import {
|
||||
|
@ -299,7 +300,7 @@ ${getRichDescription(this.calEvent)}
|
|||
}
|
||||
|
||||
protected getLocation(): string {
|
||||
let providerName = this.calEvent.location ? getIntegrationName(this.calEvent.location) : "";
|
||||
let providerName = this.calEvent.location ? getAppName(this.calEvent.location) : "";
|
||||
|
||||
if (this.calEvent.location && this.calEvent.location.includes("integrations:")) {
|
||||
const location = this.calEvent.location.split(":")[1];
|
||||
|
|
|
@ -1,32 +1,2 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export function getErrorFromUnknown(cause: unknown): Error & { statusCode?: number; code?: string } {
|
||||
if (cause instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
return cause;
|
||||
}
|
||||
if (cause instanceof Error) {
|
||||
return cause;
|
||||
}
|
||||
if (typeof cause === "string") {
|
||||
// @ts-expect-error https://github.com/tc39/proposal-error-cause
|
||||
return new Error(cause, { cause });
|
||||
}
|
||||
|
||||
return new Error(`Unhandled error of type '${typeof cause}''`);
|
||||
}
|
||||
|
||||
export function handleErrorsJson(response: Response) {
|
||||
if (!response.ok) {
|
||||
response.json().then(console.log);
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function handleErrorsRaw(response: Response) {
|
||||
if (!response.ok) {
|
||||
response.text().then(console.log);
|
||||
throw Error(response.statusText);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
// TODO: Remove this file once everything is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/errors";
|
||||
|
|
|
@ -3,14 +3,19 @@ import async from "async";
|
|||
import merge from "lodash/merge";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { FAKE_HUDDLE_CREDENTIAL } from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
|
||||
import { FAKE_JITSI_CREDENTIAL } from "@lib/integrations/Jitsi/JitsiVideoApiAdapter";
|
||||
import { createEvent, updateEvent } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { AdditionInformation, CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||
import { FAKE_HUDDLE_CREDENTIAL } from "@calcom/app-store/huddle01video/lib/VideoApiAdapter";
|
||||
import { FAKE_JITSI_CREDENTIAL } from "@calcom/app-store/jitsivideo/lib/VideoApiAdapter";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { LocationType } from "@lib/location";
|
||||
import prisma from "@lib/prisma";
|
||||
import { createMeeting, updateMeeting, VideoCallData } from "@lib/videoClient";
|
||||
import { createMeeting, updateMeeting } from "@lib/videoClient";
|
||||
|
||||
import { AdditionInformation } from "../apps/calendar/interfaces/Calendar";
|
||||
import { createEvent, updateEvent } from "../apps/calendar/managers/CalendarManager";
|
||||
|
||||
export type Event = AdditionInformation & VideoCallData;
|
||||
|
||||
|
@ -33,15 +38,6 @@ export interface PartialBooking {
|
|||
references: Array<PartialReference>;
|
||||
}
|
||||
|
||||
export interface PartialReference {
|
||||
id?: number;
|
||||
type: string;
|
||||
uid: string;
|
||||
meetingId?: string | null;
|
||||
meetingPassword?: string | null;
|
||||
meetingUrl?: string | null;
|
||||
}
|
||||
|
||||
export const isZoom = (location: string): boolean => {
|
||||
return location === "integrations:zoom";
|
||||
};
|
||||
|
@ -58,13 +54,17 @@ export const isTandem = (location: string): boolean => {
|
|||
return location === "integrations:tandem";
|
||||
};
|
||||
|
||||
export const isTeams = (location: string): boolean => {
|
||||
return location === "integrations:office365_video";
|
||||
};
|
||||
|
||||
export const isJitsi = (location: string): boolean => {
|
||||
return location === "integrations:jitsi";
|
||||
};
|
||||
|
||||
export const isDedicatedIntegration = (location: string): boolean => {
|
||||
return (
|
||||
isZoom(location) || isDaily(location) || isHuddle01(location) || isTandem(location) || isJitsi(location)
|
||||
isZoom(location) || isDaily(location) || isHuddle01(location) || isTandem(location) || isJitsi(location) || isTeams(location)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -75,7 +75,8 @@ export const getLocationRequestFromIntegration = (location: string) => {
|
|||
location === LocationType.Daily.valueOf() ||
|
||||
location === LocationType.Jitsi.valueOf() ||
|
||||
location === LocationType.Huddle01.valueOf() ||
|
||||
location === LocationType.Tandem.valueOf()
|
||||
location === LocationType.Tandem.valueOf() ||
|
||||
location === LocationType.Teams.valueOf()
|
||||
) {
|
||||
const requestId = uuidv5(location, uuidv5.URL);
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
const useMediaQuery = (query: string) => {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
const listener = () => setMatches(media.matches);
|
||||
window.addEventListener("resize", listener);
|
||||
return () => window.removeEventListener("resize", listener);
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
export default useMediaQuery;
|
|
@ -1,34 +0,0 @@
|
|||
export function getIntegrationName(name: string) {
|
||||
switch (name) {
|
||||
case "google_calendar":
|
||||
return "Google Calendar";
|
||||
case "office365_calendar":
|
||||
return "Office 365 Calendar";
|
||||
case "zoom_video":
|
||||
return "Zoom";
|
||||
case "caldav_calendar":
|
||||
return "CalDav Server";
|
||||
case "stripe_payment":
|
||||
return "Stripe";
|
||||
case "apple_calendar":
|
||||
return "Apple Calendar";
|
||||
case "daily_video":
|
||||
return "Daily";
|
||||
case "jitsi_video":
|
||||
return "Jitsi Meet";
|
||||
case "huddle01_video":
|
||||
return "Huddle01";
|
||||
case "tandem_video":
|
||||
return "Tandem";
|
||||
}
|
||||
}
|
||||
|
||||
export function getIntegrationType(name: string): string {
|
||||
if (name.endsWith("_calendar")) {
|
||||
return "Calendar";
|
||||
}
|
||||
if (name.endsWith("_payment")) {
|
||||
return "Payment";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { CalendarEvent } from "../calendar/interfaces/Calendar";
|
||||
import { handleErrorsJson, handleErrorsRaw } from "@lib/errors";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
interface TandemToken {
|
||||
expires_in?: number;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
export const CALDAV_CALENDAR_TYPE = "caldav";
|
||||
|
||||
export const APPLE_CALENDAR_URL = "https://caldav.icloud.com";
|
||||
|
||||
export const CALENDAR_INTEGRATIONS_TYPES = {
|
||||
apple: "apple_calendar",
|
||||
caldav: "caldav_calendar",
|
||||
google: "google_calendar",
|
||||
office365: "office365_calendar",
|
||||
};
|
|
@ -1,5 +0,0 @@
|
|||
import { calendar_v3 } from "googleapis";
|
||||
|
||||
export interface ConferenceData {
|
||||
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { APPLE_CALENDAR_URL, CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
export default class AppleCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, CALENDAR_INTEGRATIONS_TYPES.apple, APPLE_CALENDAR_URL);
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { CALENDAR_INTEGRATIONS_TYPES } from "../constants/generals";
|
||||
import CalendarService from "./BaseCalendarService";
|
||||
|
||||
export default class CalDavCalendarService extends CalendarService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, CALENDAR_INTEGRATIONS_TYPES.caldav);
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
|
||||
type CredentialData = Prisma.CredentialGetPayload<typeof credentialData>;
|
||||
|
||||
export type Integration = {
|
||||
installed: boolean;
|
||||
type:
|
||||
| "google_calendar"
|
||||
| "office365_calendar"
|
||||
| "zoom_video"
|
||||
| "daily_video"
|
||||
| "tandem_video"
|
||||
| "caldav_calendar"
|
||||
| "apple_calendar"
|
||||
| "stripe_payment"
|
||||
| "jitsi_video"
|
||||
| "huddle01_video"
|
||||
| "metamask_web3";
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
description: string;
|
||||
variant: "calendar" | "conferencing" | "payment" | "web3";
|
||||
};
|
||||
|
||||
export const ALL_INTEGRATIONS = [
|
||||
{
|
||||
installed: !!(process.env.GOOGLE_API_CREDENTIALS && validJson(process.env.GOOGLE_API_CREDENTIALS)),
|
||||
type: "google_calendar",
|
||||
title: "Google Calendar",
|
||||
imageSrc: "integrations/google-calendar.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.MS_GRAPH_CLIENT_ID && process.env.MS_GRAPH_CLIENT_SECRET),
|
||||
type: "office365_calendar",
|
||||
title: "Office 365 / Outlook.com Calendar",
|
||||
imageSrc: "integrations/outlook.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.ZOOM_CLIENT_ID && process.env.ZOOM_CLIENT_SECRET),
|
||||
type: "zoom_video",
|
||||
title: "Zoom",
|
||||
imageSrc: "integrations/zoom.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: !!process.env.DAILY_API_KEY,
|
||||
type: "daily_video",
|
||||
title: "Daily.co Video",
|
||||
imageSrc: "integrations/daily.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "jitsi_video",
|
||||
title: "Jitsi Meet",
|
||||
imageSrc: "integrations/jitsi.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "huddle01_video",
|
||||
title: "Huddle01",
|
||||
imageSrc: "integrations/huddle.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: !!(process.env.TANDEM_CLIENT_ID && process.env.TANDEM_CLIENT_SECRET),
|
||||
type: "tandem_video",
|
||||
title: "Tandem Video",
|
||||
imageSrc: "integrations/tandem.svg",
|
||||
description: "Virtual Office | Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "caldav_calendar",
|
||||
title: "CalDav Server",
|
||||
imageSrc: "integrations/caldav.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "apple_calendar",
|
||||
title: "Apple Calendar",
|
||||
imageSrc: "integrations/apple-calendar.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "calendar",
|
||||
},
|
||||
{
|
||||
installed: !!(
|
||||
process.env.STRIPE_CLIENT_ID &&
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_PRIVATE_KEY
|
||||
),
|
||||
type: "stripe_payment",
|
||||
title: "Stripe",
|
||||
imageSrc: "integrations/stripe.svg",
|
||||
description: "Collect payments",
|
||||
variant: "payment",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "metamask_web3",
|
||||
title: "Metamask",
|
||||
imageSrc: "integrations/apple-calendar.svg",
|
||||
description: "For personal and business calendars",
|
||||
variant: "web3",
|
||||
},
|
||||
] as Integration[];
|
||||
|
||||
function getIntegrations(userCredentials: CredentialData[]) {
|
||||
const integrations = ALL_INTEGRATIONS.map((integration) => {
|
||||
const credentials = userCredentials
|
||||
.filter((credential) => credential.type === integration.type)
|
||||
.map((credential) => _.pick(credential, ["id", "type"])); // ensure we don't leak `key` to frontend
|
||||
|
||||
const credential: typeof credentials[number] | null = credentials[0] || null;
|
||||
return {
|
||||
...integration,
|
||||
/**
|
||||
* @deprecated use `credentials`
|
||||
*/
|
||||
credential,
|
||||
credentials,
|
||||
};
|
||||
});
|
||||
|
||||
return integrations;
|
||||
}
|
||||
|
||||
export type IntegrationMeta = ReturnType<typeof getIntegrations>;
|
||||
|
||||
export function hasIntegration(integrations: IntegrationMeta, type: string): boolean {
|
||||
return !!integrations.find(
|
||||
(i) =>
|
||||
i.type === type &&
|
||||
!!i.installed &&
|
||||
(type === "daily_video" ||
|
||||
type === "jitsi_video" ||
|
||||
type === "huddle01_video" ||
|
||||
i.credentials.length > 0)
|
||||
);
|
||||
}
|
||||
export function hasIntegrationInstalled(type: Integration["type"]): boolean {
|
||||
return ALL_INTEGRATIONS.some((i) => i.type === type && !!i.installed);
|
||||
}
|
||||
|
||||
export default getIntegrations;
|
|
@ -2,9 +2,9 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { getBusyCalendarTimes } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export async function getUserAvailability(query: {
|
||||
|
|
|
@ -1,9 +1,2 @@
|
|||
export const randomString = function (length = 12) {
|
||||
let result = "";
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
// TODO: Remove this file once everything is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/random";
|
||||
|
|
|
@ -2,57 +2,33 @@ import { Credential } from "@prisma/client";
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoApiAdapterFactory } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import { getUid } from "@lib/CalEventParser";
|
||||
import { EventResult } from "@lib/events/EventManager";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
|
||||
import JitsiVideoApiAdapter from "@lib/integrations/Jitsi/JitsiVideoApiAdapter";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||
import TandemVideoApiAdapter from "./integrations/Tandem/TandemVideoApiAdapter";
|
||||
import ZoomVideoApiAdapter from "./integrations/Zoom/ZoomVideoApiAdapter";
|
||||
import { CalendarEvent } from "./integrations/calendar/interfaces/Calendar";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["[lib] videoClient"] });
|
||||
|
||||
const translator = short();
|
||||
|
||||
export interface VideoCallData {
|
||||
type: string;
|
||||
id: string;
|
||||
password: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type EventBusyDate = Record<"start" | "end", Date>;
|
||||
|
||||
export interface VideoApiAdapter {
|
||||
createMeeting(event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
updateMeeting(bookingRef: PartialReference, event: CalendarEvent): Promise<VideoCallData>;
|
||||
|
||||
deleteMeeting(uid: string): Promise<unknown>;
|
||||
|
||||
getAvailability(dateFrom?: string, dateTo?: string): Promise<EventBusyDate[]>;
|
||||
}
|
||||
|
||||
// factory
|
||||
const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
||||
withCredentials.reduce<VideoApiAdapter[]>((acc, cred) => {
|
||||
const appName = cred.type.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||
const makeVideoApiAdapter = appStore[appName].lib?.VideoApiAdapter as VideoApiAdapterFactory;
|
||||
if (typeof makeVideoApiAdapter !== "undefined") {
|
||||
const videoAdapter = makeVideoApiAdapter(cred);
|
||||
acc.push(videoAdapter);
|
||||
return acc;
|
||||
}
|
||||
|
||||
switch (cred.type) {
|
||||
case "zoom_video":
|
||||
acc.push(ZoomVideoApiAdapter(cred));
|
||||
break;
|
||||
case "daily_video":
|
||||
acc.push(DailyVideoApiAdapter(cred));
|
||||
break;
|
||||
case "jitsi_video":
|
||||
acc.push(JitsiVideoApiAdapter());
|
||||
break;
|
||||
case "huddle01_video":
|
||||
acc.push(Huddle01VideoApiAdapter());
|
||||
break;
|
||||
case "tandem_video":
|
||||
acc.push(TandemVideoApiAdapter(cred));
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { compile } from "handlebars";
|
||||
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
type ContentType = "application/json" | "application/x-www-form-urlencoded";
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
const withTM = require("next-transpile-modules")(["@calcom/lib", "@calcom/prisma", "@calcom/ui"]);
|
||||
const withTM = require("next-transpile-modules")([
|
||||
"@calcom/app-store",
|
||||
"@calcom/lib",
|
||||
"@calcom/prisma",
|
||||
"@calcom/ui",
|
||||
]);
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
|
||||
// So we can test deploy previews preview
|
||||
|
|
|
@ -27,11 +27,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "0.3.6",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@daily-co/daily-js": "^0.21.0",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@headlessui/react": "^1.4.2",
|
||||
"@heroicons/react": "^1.0.5",
|
||||
"@hookform/error-message": "^2.0.0",
|
||||
|
@ -106,12 +108,15 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/types": "*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.18.1",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.1",
|
||||
"@types/accept-language-parser": "1.5.2",
|
||||
"@types/async": "^3.2.10",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/glidejs__glide": "^3.4.1",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/micro": "^7.3.6",
|
||||
|
|
|
@ -5,9 +5,9 @@ import timezone from "dayjs/plugin/timezone";
|
|||
import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getBusyCalendarTimes } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { Prisma, User, Booking, SchedulingType, BookingStatus } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { AdditionInformation } from "@lib/apps/calendar/interfaces/Calendar";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { sendDeclinedEmails } from "@lib/emails/email-manager";
|
||||
import { sendScheduledEmails } from "@lib/emails/email-manager";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingConfirmBody } from "@lib/types/booking";
|
||||
|
|
|
@ -9,22 +9,23 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import { handlePayment } from "@ee/lib/stripe/server";
|
||||
|
||||
import { AdditionInformation } from "@lib/apps/calendar/interfaces/Calendar";
|
||||
import { getBusyCalendarTimes } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { BufferedBusyTime } from "@lib/apps/office365_calendar/types/Office365Calendar";
|
||||
import {
|
||||
sendScheduledEmails,
|
||||
sendRescheduledEmails,
|
||||
sendOrganizerRequestEmail,
|
||||
sendAttendeeRequestEmail,
|
||||
sendOrganizerRequestEmail,
|
||||
sendRescheduledEmails,
|
||||
sendScheduledEmails,
|
||||
} from "@lib/emails/email-manager";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { getEventName } from "@lib/event";
|
||||
import EventManager, { EventResult, PartialReference } from "@lib/events/EventManager";
|
||||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { EventBusyDate } from "@lib/integrations/calendar/constants/types";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
|
|
|
@ -3,14 +3,14 @@ import async from "async";
|
|||
import dayjs from "dayjs";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getCalendar } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
import { deleteMeeting } from "@lib/videoClient";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
|
@ -163,7 +163,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
|
||||
}
|
||||
|
||||
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential) => {
|
||||
const apiDeletes = async.mapLimit(bookingToDelete.user.credentials, 5, async (credential: any) => {
|
||||
const bookingRefUid = bookingToDelete.references.filter((ref) => ref.type === credential.type)[0]?.uid;
|
||||
if (bookingRefUid) {
|
||||
if (credential.type.endsWith("_calendar")) {
|
||||
|
|
|
@ -2,8 +2,9 @@ import { ReminderType } from "@prisma/client";
|
|||
import dayjs from "dayjs";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import type { CalendarEvent } from "@calcom/types/CalendarEvent";
|
||||
|
||||
import { sendOrganizerRequestReminderEmail } from "@lib/emails/email-manager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import appStore from "@calcom/app-store";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Check that user is authenticated
|
||||
req.session = await getSession({ req });
|
||||
|
||||
if (!req.session?.user?.id) {
|
||||
res.status(401).json({ message: "You must be logged in to do this" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { args } = req.query;
|
||||
|
||||
if (!Array.isArray(args)) {
|
||||
return res.status(404).json({ message: `API route not found` });
|
||||
}
|
||||
|
||||
const [_appName, apiEndpoint] = args;
|
||||
const appName = _appName.split("_").join(""); // Transform `zoom_video` to `zoomvideo`;
|
||||
|
||||
try {
|
||||
// TODO: Find a way to dynamically import these modules
|
||||
// const app = (await import(`@calcom/${appName}`)).default;
|
||||
const handler = appStore[appName].api[apiEndpoint];
|
||||
if (typeof handler !== "function")
|
||||
throw new HttpError({ statusCode: 404, message: `API handler not found` });
|
||||
|
||||
const response = await handler(req, res);
|
||||
console.log("response", response);
|
||||
|
||||
res.status(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.statusCode).json({ message: error.message });
|
||||
}
|
||||
return res.status(404).json({ message: `API handler not found` });
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
|
@ -1,8 +1,8 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendar } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { symmetricEncrypt } from "@lib/crypto";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendar } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { symmetricEncrypt } from "@lib/crypto";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
||||
import { InferGetStaticPropsType } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Shell from "@components/Shell";
|
||||
import AppCard from "@components/apps/AppCard";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export default function Apps({ appStore }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Shell
|
||||
heading={router.query.category + " - " + t("app_store")}
|
||||
subtitle={t("app_store_description")}
|
||||
large>
|
||||
<div className="mb-8">
|
||||
<Button color="secondary" href="/apps">
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mb-16">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">All {router.query.category} apps</h2>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{appStore.map((app) => {
|
||||
return (
|
||||
app.category === router.query.category && (
|
||||
<AppCard
|
||||
key={app.name}
|
||||
slug={app.slug}
|
||||
name={app.name}
|
||||
description={app.description}
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const paths = appStore.reduce((categories, app) => {
|
||||
if (!categories.includes(app.category)) {
|
||||
categories.push(app.category);
|
||||
}
|
||||
return categories;
|
||||
}, [] as string[]);
|
||||
|
||||
return {
|
||||
paths: paths.map((category) => ({ params: { category } })),
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
appStore: getAppRegistry(),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import { InferGetStaticPropsType } from "next";
|
||||
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import AppsShell from "@components/AppsShell";
|
||||
import Shell from "@components/Shell";
|
||||
import AllApps from "@components/apps/AllApps";
|
||||
import AppStoreCategories from "@components/apps/Categories";
|
||||
import Slider from "@components/apps/Slider";
|
||||
|
||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell heading={t("app_store")} subtitle={t("app_store_description")} large>
|
||||
<AppsShell>
|
||||
<AppStoreCategories categories={categories} />
|
||||
<Slider items={appStore} />
|
||||
<AllApps apps={appStore} />
|
||||
</AppsShell>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const appStore = getAppRegistry();
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
if (c[app.category]) {
|
||||
c[app.category] = c[app.category]++;
|
||||
} else {
|
||||
c[app.category] = 1;
|
||||
}
|
||||
return c;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
props: {
|
||||
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
|
||||
appStore,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
|
|||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { CalendarListContainer } from "@lib/apps/calendar/components/CalendarListContainer";
|
||||
import classNames from "@lib/classNames";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
@ -14,7 +15,6 @@ import { ClientSuspense } from "@components/ClientSuspense";
|
|||
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";
|
||||
import Loader from "@components/Loader";
|
||||
import Shell, { ShellSubHeading } from "@components/Shell";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
import IntegrationListItem from "@components/integrations/IntegrationListItem";
|
||||
|
@ -40,7 +40,7 @@ function IframeEmbedContainer() {
|
|||
<List>
|
||||
<ListItem className={classNames("flex-col")}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
||||
<Image width={40} height={40} src="/integrations/embed.svg" alt="Embed" />
|
||||
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3">{t("standard_iframe")}</ListItemTitle>
|
||||
<ListItemText component="p">{t("embed_your_calendar")}</ListItemText>
|
||||
|
@ -65,7 +65,7 @@ function IframeEmbedContainer() {
|
|||
</ListItem>
|
||||
<ListItem className={classNames("flex-col")}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3 rtl:space-x-reverse")}>
|
||||
<Image width={40} height={40} src="/integrations/embed.svg" alt="Embed" />
|
||||
<Image width={40} height={40} src="/apps/embed.svg" alt="Embed" />
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3">{t("responsive_fullscreen_iframe")}</ListItemTitle>
|
||||
<ListItemText component="p">A fullscreen scheduling experience on your website</ListItemText>
|
||||
|
@ -108,6 +108,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
//
|
||||
credentialIds: number[];
|
||||
type: string;
|
||||
isGlobal?: boolean;
|
||||
installed: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
@ -138,7 +139,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
);
|
||||
}
|
||||
/** We don't need to "Connect", just show that it's installed */
|
||||
if (["daily_video", "huddle01_video", "jitsi_video"].includes(props.type)) {
|
||||
if (props.isGlobal) {
|
||||
return (
|
||||
<div className="truncate px-3 py-2">
|
||||
<h3 className="text-sm font-medium text-gray-700">{t("installed")}</h3>
|
||||
|
@ -214,7 +215,7 @@ function Web3Container() {
|
|||
<List>
|
||||
<ListItem className={classNames("flex-col")}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-3")}>
|
||||
<Image width={40} height={40} src="/integrations/metamask.svg" alt="Embed" />
|
||||
<Image width={40} height={40} src="/apps/metamask.svg" alt="Embed" />
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3">
|
||||
MetaMask (
|
||||
|
@ -285,7 +286,7 @@ export default function IntegrationsPage() {
|
|||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell heading={t("integrations")} subtitle={t("connect_your_favourite_apps")}>
|
||||
<Shell heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<IntegrationsContainer />
|
||||
<CalendarListContainer />
|
|
@ -0,0 +1,117 @@
|
|||
import showToast from "@lib/notification";
|
||||
|
||||
import App from "@components/App";
|
||||
|
||||
export default function NukeMyCal() {
|
||||
return (
|
||||
<App
|
||||
name="Nuke my Cal"
|
||||
logo="/apps/nuke-my-cal.svg"
|
||||
categories={["fun", "productivity"]}
|
||||
author="/peer"
|
||||
type="free" // "usage-based", "monthly", "one-time" or "free"
|
||||
price={0} // 0 = free. if type="usage-based" it's the price per booking
|
||||
commission={0} // only required for "usage-based" billing. % of commission for paid bookings
|
||||
website="https://cal.com"
|
||||
email="help@cal.com"
|
||||
tos="https://cal.com/terms"
|
||||
privacy="https://cal.com/privacy"
|
||||
body={
|
||||
<>
|
||||
<style jsx>
|
||||
{`
|
||||
.pushable {
|
||||
position: relative;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
outline-offset: 4px;
|
||||
transition: filter 250ms;
|
||||
}
|
||||
.shadow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
background: hsl(0deg 0% 0% / 0.25);
|
||||
will-change: transform;
|
||||
transform: translateY(2px);
|
||||
transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
|
||||
}
|
||||
.edge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
hsl(340deg 100% 16%) 0%,
|
||||
hsl(340deg 100% 32%) 8%,
|
||||
hsl(340deg 100% 32%) 92%,
|
||||
hsl(340deg 100% 16%) 100%
|
||||
);
|
||||
}
|
||||
.front {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding: 12px 42px;
|
||||
border-radius: 12px;
|
||||
font-size: 1.25rem;
|
||||
color: white;
|
||||
background: hsl(345deg 100% 47%);
|
||||
will-change: transform;
|
||||
transform: translateY(-4px);
|
||||
transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
|
||||
}
|
||||
.pushable:hover {
|
||||
filter: brightness(110%);
|
||||
}
|
||||
.pushable:hover .front {
|
||||
transform: translateY(-6px);
|
||||
transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
|
||||
}
|
||||
.pushable:active .front {
|
||||
transform: translateY(-2px);
|
||||
transition: transform 34ms;
|
||||
}
|
||||
.pushable:hover .shadow {
|
||||
transform: translateY(4px);
|
||||
transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
|
||||
}
|
||||
.pushable:active .shadow {
|
||||
transform: translateY(1px);
|
||||
transition: transform 34ms;
|
||||
}
|
||||
.pushable:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div>
|
||||
Have an emergency? Need to reschedule all of your upcoming calendar events? Just click{" "}
|
||||
<strong>Nuke my Cal</strong> and auto-reschedule the entire day. Give it a try!
|
||||
<br />
|
||||
<br />
|
||||
Demo: <br />
|
||||
<br />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => (
|
||||
new Audio("/apps/nuke-my-cal.wav").play(),
|
||||
showToast("All of your calendar events for today have been rescheduled", "success")
|
||||
)}
|
||||
className="pushable">
|
||||
<span className="shadow"></span>
|
||||
<span className="edge"></span>
|
||||
<span className="front">Nuke my Cal</span>
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import App from "@components/App";
|
||||
|
||||
export default function Stripe() {
|
||||
return (
|
||||
<App
|
||||
name="Stripe"
|
||||
logo="/apps/stripe.svg"
|
||||
categories={["payments"]}
|
||||
author="Cal.com"
|
||||
type="usage-based" // "usage-based" or "monthly" or "one-time"
|
||||
price={0.1} // price per installation. if "monthly" = price per month. if type="usage-based" = price per booking
|
||||
commission={0} // only required for "usage-based" billing. % of commission for paid bookings
|
||||
docs="https://stripe.com/docs"
|
||||
website="https://zoom.us"
|
||||
email="support@zoom.us"
|
||||
tos="https://zoom.us/terms"
|
||||
privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
<>
|
||||
Stripe provides payments infrastructure for the internet. Millions of businesses of all sizes—from
|
||||
startups to large enterprises—use Stripe's software and APIs to accept payments, send payouts,
|
||||
and manage their businesses online.
|
||||
<br />
|
||||
<br />
|
||||
Use this Stripe App, build by the Cal.com team to start charging for your bookings today.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import App from "@components/App";
|
||||
|
||||
export default function Zoom() {
|
||||
return (
|
||||
<App
|
||||
name="Zoom"
|
||||
logo="/apps/zoom.svg"
|
||||
categories={["video", "communication"]}
|
||||
author="Cal.com"
|
||||
type="usage-based" // "usage-based" or "monthly" or "one-time"
|
||||
price={0} // 0 = free. if type="usage-based" it's the price per booking
|
||||
commission={0.5} // only required for "usage-based" billing. % of commission for paid bookings
|
||||
docs="https://zoom.us/download"
|
||||
website="https://zoom.us"
|
||||
email="support@zoom.us"
|
||||
tos="https://zoom.us/terms"
|
||||
privacy="https://zoom.us/privacy"
|
||||
body={
|
||||
<>
|
||||
Start Zoom Meetings and make Zoom Phone calls with flawless video, crystal clear audio, and instant
|
||||
screen sharing from any Slack channel, private group, or direct message using the /zoom slash
|
||||
command.
|
||||
<br />
|
||||
<br />
|
||||
The Zoom app for Slack can be installed individually by any Slack user with a Zoom account or be
|
||||
deployed to the whole organization centrally by the Zoom account admin with a few simple steps.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -287,6 +287,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
||||
case LocationType.Tandem:
|
||||
return <p className="text-sm">{t("cal_provide_tandem_meeting_url")}</p>;
|
||||
case LocationType.Teams:
|
||||
return <p className="text-sm">{t("cal_provide_teams_meeting_url")}</p>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -410,7 +412,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
{formMethods.getValues("locations").map((location) => (
|
||||
<li
|
||||
key={location.type}
|
||||
className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2 shadow-sm">
|
||||
className="border-neutral-300 mb-2 rounded-sm border py-1.5 px-2 shadow-sm">
|
||||
<div className="flex justify-between">
|
||||
{location.type === LocationType.InPerson && (
|
||||
<div className="flex flex-grow items-center">
|
||||
|
@ -610,6 +612,78 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<span className="ml-2 text-sm">Jitsi Meet</span>
|
||||
</div>
|
||||
)}
|
||||
{location.type === LocationType.Teams && (
|
||||
<div className="flex flex-grow items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
viewBox="0 0 2228.833 2073.333">
|
||||
<path
|
||||
fill="#5059C9"
|
||||
d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"
|
||||
/>
|
||||
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25" />
|
||||
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917" />
|
||||
<path
|
||||
fill="#7B83EB"
|
||||
d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"
|
||||
/>
|
||||
<path
|
||||
opacity=".1"
|
||||
d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"
|
||||
/>
|
||||
<path
|
||||
opacity=".1"
|
||||
d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"
|
||||
/>
|
||||
<path
|
||||
opacity=".2"
|
||||
d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="a"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="198.099"
|
||||
y1="1683.0726"
|
||||
x2="942.2344"
|
||||
y2="394.2607"
|
||||
gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
|
||||
<stop offset="0" stopColor="#5a62c3" />
|
||||
<stop offset=".5" stopColor="#4d55bd" />
|
||||
<stop offset="1" stopColor="#3940ab" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"
|
||||
/>
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2 text-sm">MS Teams</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -631,8 +705,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
type="button"
|
||||
className="flex rounded-sm py-2 hover:bg-gray-100"
|
||||
onClick={() => setShowLocationModal(true)}>
|
||||
<PlusIcon className="mt-0.5 h-4 w-4 text-neutral-900" />
|
||||
<span className="ml-1 text-sm font-medium text-neutral-700">{t("add_location")}</span>
|
||||
<PlusIcon className="text-neutral-900 mt-0.5 h-4 w-4" />
|
||||
<span className="text-neutral-700 ml-1 text-sm font-medium">{t("add_location")}</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
@ -668,7 +742,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
autoFocus
|
||||
style={{ top: -6, fontSize: 22 }}
|
||||
required
|
||||
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
|
||||
className="focus:outline-none relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:ring-0"
|
||||
placeholder={t("quick_chat")}
|
||||
{...formMethods.register("title")}
|
||||
defaultValue={eventType.title}
|
||||
|
@ -681,7 +755,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<ClientSuspense fallback={<Loader />}>
|
||||
<div className="mx-auto block sm:flex md:max-w-5xl">
|
||||
<div className="w-full ltr:mr-2 rtl:ml-2 sm:w-9/12">
|
||||
<div className="-mx-4 rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div className="border-neutral-200 -mx-4 rounded-sm border bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<Form
|
||||
form={formMethods}
|
||||
handleSubmit={async (values) => {
|
||||
|
@ -713,8 +787,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="space-y-3">
|
||||
<div className="block items-center sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor="slug" className="flex text-sm font-medium text-neutral-700">
|
||||
<LinkIcon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
<label htmlFor="slug" className="text-neutral-700 flex text-sm font-medium">
|
||||
<LinkIcon className="text-neutral-500 mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("url")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -744,7 +818,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<MinutesField
|
||||
label={
|
||||
<>
|
||||
<ClockIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
<ClockIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
{t("duration")}
|
||||
</>
|
||||
}
|
||||
|
@ -766,8 +840,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 sm:mb-0">
|
||||
<label
|
||||
htmlFor="location"
|
||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
||||
<LocationMarkerIcon className="mt-0.5 mb-4 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||
<LocationMarkerIcon className="text-neutral-500 mt-0.5 mb-4 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("location")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -785,8 +859,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 mt-2.5 sm:mb-0">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="mt-0 flex text-sm font-medium text-neutral-700">
|
||||
<DocumentIcon className="mt-0.5 h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
className="text-neutral-700 mt-0 flex text-sm font-medium">
|
||||
<DocumentIcon className="text-neutral-500 mt-0.5 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("description")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -807,8 +881,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="schedulingType"
|
||||
className="mt-2 flex text-sm font-medium text-neutral-700">
|
||||
<UsersIcon className="h-5 w-5 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
className="text-neutral-700 mt-2 flex text-sm font-medium">
|
||||
<UsersIcon className="text-neutral-500 h-5 w-5 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -831,8 +905,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
<div className="block sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor="users" className="flex text-sm font-medium text-neutral-700">
|
||||
<UserAddIcon className="h-5 w-5 text-neutral-500 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
<label htmlFor="users" className="text-neutral-700 flex text-sm font-medium">
|
||||
<UserAddIcon className="text-neutral-500 h-5 w-5 ltr:mr-2 rtl:ml-2" />{" "}
|
||||
{t("attendees")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -868,9 +942,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<ChevronRightIcon
|
||||
className={`${
|
||||
advancedSettingsVisible ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
} text-neutral-500 ml-auto h-5 w-5`}
|
||||
/>
|
||||
<span className="text-sm font-medium text-neutral-700">
|
||||
<span className="text-neutral-700 text-sm font-medium">
|
||||
{t("show_advanced_settings")}
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
|
@ -885,7 +959,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="createEventsOn"
|
||||
className="flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 flex text-sm font-medium">
|
||||
{t("create_events_on")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -909,7 +983,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
)}
|
||||
<div className="block items-center sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
||||
<label htmlFor="eventName" className="text-neutral-700 flex text-sm font-medium">
|
||||
{t("event_name")} <InfoBadge content={t("event_name_tooltip")} />
|
||||
</label>
|
||||
</div>
|
||||
|
@ -930,7 +1004,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="smartContractAddress"
|
||||
className="flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 flex text-sm font-medium">
|
||||
{t("Smart Contract Address")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -953,7 +1027,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="additionalFields"
|
||||
className="flexflex mt-2 text-sm font-medium text-neutral-700">
|
||||
className="flexflex text-neutral-700 mt-2 text-sm font-medium">
|
||||
{t("additional_inputs")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1059,7 +1133,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
)}
|
||||
/>
|
||||
|
||||
<hr className="my-2 border-neutral-200" />
|
||||
<hr className="border-neutral-200 my-2" />
|
||||
<Controller
|
||||
name="minimumBookingNotice"
|
||||
control={formMethods.control}
|
||||
|
@ -1080,7 +1154,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
<div className="block items-center sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor="eventName" className="flex text-sm font-medium text-neutral-700">
|
||||
<label htmlFor="eventName" className="text-neutral-700 flex text-sm font-medium">
|
||||
{t("slot_interval")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1129,7 +1203,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="inviteesCanSchedule"
|
||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||
{t("invitees_can_schedule")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1149,7 +1223,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
className="focus:outline-none flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||
</RadioGroup.Item>
|
||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||
|
@ -1164,7 +1238,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
/>
|
||||
<select
|
||||
id=""
|
||||
className="focus:border-primary-500 focus:ring-primary-500 block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none sm:text-sm"
|
||||
className="focus:border-primary-500 focus:ring-primary-500 focus:outline-none block w-full rounded-sm border-gray-300 py-2 pl-3 pr-10 text-base sm:text-sm"
|
||||
{...formMethods.register("periodCountCalendarDays")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
|
@ -1205,7 +1279,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="bufferTime"
|
||||
className="mt-2.5 flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 mt-2.5 flex text-sm font-medium">
|
||||
{t("buffer_time")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1214,7 +1288,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="w-full">
|
||||
<label
|
||||
htmlFor="beforeBufferTime"
|
||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 mb-2 flex text-sm font-medium">
|
||||
{t("before_event")}
|
||||
</label>
|
||||
<Controller
|
||||
|
@ -1253,7 +1327,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="w-full">
|
||||
<label
|
||||
htmlFor="afterBufferTime"
|
||||
className="mb-2 flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 mb-2 flex text-sm font-medium">
|
||||
{t("after_event")}
|
||||
</label>
|
||||
<Controller
|
||||
|
@ -1298,7 +1372,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="availability"
|
||||
className="flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 flex text-sm font-medium">
|
||||
{t("availability")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1341,7 +1415,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label
|
||||
htmlFor="payment"
|
||||
className="mt-2 flex text-sm font-medium text-neutral-700">
|
||||
className="text-neutral-700 mt-2 flex text-sm font-medium">
|
||||
{t("payment")}
|
||||
</label>
|
||||
</div>
|
||||
|
@ -1466,9 +1540,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-md inline-flex items-center rounded-sm px-2 py-1 text-sm font-medium text-neutral-700 hover:bg-gray-200 hover:text-gray-900">
|
||||
className="text-md text-neutral-700 inline-flex items-center rounded-sm px-2 py-1 text-sm font-medium hover:bg-gray-200 hover:text-gray-900">
|
||||
<ExternalLinkIcon
|
||||
className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2"
|
||||
className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("preview")}
|
||||
|
@ -1480,12 +1554,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
}}
|
||||
type="button"
|
||||
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900">
|
||||
<LinkIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
<LinkIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("copy_link")}
|
||||
</button>
|
||||
<Dialog>
|
||||
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-neutral-700 hover:bg-gray-200 hover:text-gray-900">
|
||||
<TrashIcon className="h-4 w-4 text-neutral-500 ltr:mr-2 rtl:ml-2" />
|
||||
<DialogTrigger className="text-md text-neutral-700 flex items-center rounded-sm px-2 py-1 text-sm font-medium hover:bg-gray-200 hover:text-gray-900">
|
||||
<TrashIcon className="text-neutral-500 h-4 w-4 ltr:mr-2 rtl:ml-2" />
|
||||
{t("delete")}
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
|
|
|
@ -17,12 +17,13 @@ import { useForm } from "react-hook-form";
|
|||
import TimezoneSelect from "react-timezone-select";
|
||||
import * as z from "zod";
|
||||
|
||||
import { CalendarListContainer } from "@lib/apps/calendar/components/CalendarListContainer";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/apps/calendar/managers/CalendarManager";
|
||||
import getApps from "@lib/apps/utils/AppUtils";
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { DEFAULT_SCHEDULE } from "@lib/availability";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@lib/integrations/calendar/CalendarManager";
|
||||
import getIntegrations from "@lib/integrations/getIntegrations";
|
||||
import prisma from "@lib/prisma";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
@ -32,7 +33,6 @@ import { Schedule as ScheduleType } from "@lib/types/schedule";
|
|||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import Loader from "@components/Loader";
|
||||
import { Form } from "@components/form/fields";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Button from "@components/ui/Button";
|
||||
import Text from "@components/ui/Text";
|
||||
|
@ -704,7 +704,7 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
},
|
||||
});
|
||||
|
||||
const integrations = getIntegrations(credentials)
|
||||
const integrations = getApps(credentials)
|
||||
.filter((item) => item.type.endsWith("_calendar"))
|
||||
.map((item) => omit(item, "key"));
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
function RedirectPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return { redirect: { permanent: false, destination: "/integrations" } };
|
||||
}
|
||||
|
||||
export default RedirectPage;
|
|
@ -1,6 +1,6 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { hasIntegrationInstalled } from "../lib/integrations/getIntegrations";
|
||||
import { hasIntegrationInstalled } from "../lib/apps/utils/AppUtils";
|
||||
import { todo } from "./lib/testUtils";
|
||||
|
||||
test.describe.serial("Stripe integration", () => {
|
||||
|
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 264 B After Width: | Height: | Size: 264 B |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2228.833 2073.333">
|
||||
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
|
||||
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
|
||||
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
|
||||
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
|
||||
<path opacity=".1" d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
|
||||
<path opacity=".2" d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||
<path opacity=".2" d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
|
||||
<path opacity=".2" d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
|
||||
<path opacity=".1" d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
|
||||
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
|
||||
<path opacity=".2" d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
|
||||
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
|
||||
<stop offset="0" stop-color="#5a62c3"/>
|
||||
<stop offset=".5" stop-color="#4d55bd"/>
|
||||
<stop offset="1" stop-color="#3940ab"/>
|
||||
</linearGradient>
|
||||
<path fill="url(#a)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
|
||||
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" version="1.1" width="720" height="720" id="svg2">
|
||||
<defs id="defs4">
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3201"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3197"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3193"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3189"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3185"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3181"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3177"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3173"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3169"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3165"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3161"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3157"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3153"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3149"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3149-6"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3153-1"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3157-8"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3161-4"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3165-8"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3169-4"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3173-0"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3177-7"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3181-6"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3185-8"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3189-9"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3193-7"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3197-3"/>
|
||||
<inkscape:path-effect effect="spiro" id="path-effect3201-3"/>
|
||||
</defs>
|
||||
<path d="m 329.6885371631169,144.9454094414912 c 0,0 -9.3691648630806,-18.5615530306314 -37.6534361478519,-6.3639610390736" id="path2983" style="fill:none;stroke:#ff0000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:none"/>
|
||||
<path d="m 326.8905349710195,144.9454094414912 c 0,0 -9.3691648630806,-18.5615530306314 -37.6534361478519,-6.3639610390736" id="path2983-8" style="fill:none;stroke:#ff0000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;display:none"/>
|
||||
<path d="M 94.6324614631855,36.00250586938022 300.1705854663462,193.5309023590188 342.0166705927379,44.6173400524073 401.0934966535268,195.9922835541695 564.785535530295,54.46286483300981 476.1702964391128,237.8357638717294 616.4777583334849,232.9130014814282 525.4009848231031,347.3672270559314 647.2469385734796,426.1314253007506 502.0164078407082,428.5928064959011 630.0161976390828,582.4291311928135 446.6318834087184,497.5114799601184 426.9396080551219,662.4240200352077 337.0936017543393,529.5094354970756 211.5553463751634,700.5754285600419 223.863018471161,486.4352645819405 52.7863763367938,520.8946013140482 159.8631235719732,380.5958731904649 72.47865169038992,237.8357638717299 194.3246054407665,261.2188852256607 94.6324614631855,36.00250586938069 z" id="path3508" style="fill:#ff0000;stroke:none"/>
|
||||
<path d="m 160.1985233083172,118.7982717399246 154.3242216264676,118.2770704473064 31.419302606586,-111.8087931572194 44.3566625034159,113.6568723829586 122.9049190198811,-106.2645554800019 -66.5349937551232,137.6819023175674 105.347073445612,-3.6961584514783 -68.3831880260988,85.9356839968712 91.4856164132948,59.1385352236531 -109.0434619875631,1.8480792257391 96.1061020907337,115.5049516086972 -137.6904731876862,-63.7587332880004 -14.7855541678056,123.8213081245232 -67.4590908906114,-99.7962781899145 -94.2579078197586,128.4415061888714 9.2409713548783,-160.7828926393062 -128.4495018328083,25.8731091603477 80.3964507874411,-105.3405158671315 -65.6108966196359,-107.1885950928715 91.4856164132951,17.5567526445221 -74.8518679745143,-169.0992491551333 z" id="path3512" style="fill:#ff8000;stroke:none"/>
|
||||
<path d="m 226.2365903979516,204.2096633100054 99.1022747159463,75.9539014973134 20.1765110200129,-71.8001725091792 28.4844861459008,72.9869522200746 78.9257636959331,-68.2398333764925 -42.7267292188508,88.4150884617163 67.6506545965139,-2.373559421791 -43.9135828082631,55.1852565566419 58.7492526759201,37.9769507486567 -70.0243617753388,1.1867797108955 61.7163866494512,74.17373193097 -88.4205924112332,-40.9439000258953 -9.4948287153004,79.5142406299997 -43.3201560135574,-64.0861043883581 -60.529533060039,82.4811899072385 5.9342679470626,-103.2498348479099 -82.4863244641708,16.614915952537 51.6281311394449,-67.6464435210443 -42.1333024241446,-68.8332232319403 58.7492526759201,11.2744072535075 -48.0675703712075,-108.5903435469403 z" id="path3514" style="fill:#ffff00;stroke:none"/>
|
||||
<path d="m -57.96064186096192,305.2593994140625 a 30.91234397888183,30.91234397888183 0 1 1 -61.82468795776368,0 30.91234397888183,30.91234397888183 0 1 1 61.82468795776368,0 z" transform="translate(435.3488372093023,54.09660107334526)" id="path3516" style="fill:#000000;fill-opacity:1;stroke:none"/>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
|
||||
<dc:publisher>
|
||||
<cc:Agent rdf:about="http://openclipart.org/">
|
||||
<dc:title>Open Clip Art Library</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:title>one eyed sun</dc:title>
|
||||
<dc:date>2011-07-10T22:19:30</dc:date>
|
||||
<dc:description>my mom came up with the name for this!</dc:description>
|
||||
<dc:source>http://openclipart.org/detail/148927/one-eyed-sun-by-10binary</dc:source>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>10binary</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>clip art</rdf:li>
|
||||
<rdf:li>clipart</rdf:li>
|
||||
<rdf:li>eyed</rdf:li>
|
||||
<rdf:li>one</rdf:li>
|
||||
<rdf:li>sun</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 509 B After Width: | Height: | Size: 509 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 981 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |