Compare commits

...

49 Commits

Author SHA1 Message Date
Joe Au-Yeung 5ba094964d Create Lark package 2022-03-10 10:07:54 -05:00
Joe Au-Yeung 26db39f98b
Monorepo/app store MS Teams Integration (#2080)
* Create teamsvideo package

* Remove zoom specific refrences

* Add teams video files

* Rename to office365_video

* Add call back to add crednetial type office365_teams

* Rename to office_video to match type

* Add MS Teams as a location option

* Rename files

* Add teams reponse interface and create meeting

* Comment out Daily imports

* Add check for Teams integration

* Add token checking functions

* Change template to create event rather than meeting

* Add comment to test between create link and event

* Add teams URL to booking

* Ask for just onlineMeeting permission

* Add MS Teams logo

* Add message to have an enterprise account

* Remove comments

* Comment back hasDailyIntegration

* Comment back daily credentials

* Update link to MS Graph section of README

* Move API calls to package

Co-authored-by: Omar López <zomars@me.com>
2022-03-08 10:22:08 -07:00
zomars 26e5904d00 Merge branch 'main' into monorepo/app-store 2022-03-07 17:30:12 -07:00
Leo Giovanetti d21d53000e
Jitsi Video App migration (#2027)
* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

Co-authored-by: Omar López <zomars@me.com>
2022-03-07 11:17:26 -07:00
Leo Giovanetti 67564fdb35
Huddle01 migration to app store (#2038)
* Jitsi Video App migration

* Removing uneeded dependencies

* Missed unused reference

* Missing dependency

`@calcom/lib` is needed in the `locationOption.ts` file

* Huddle01 migration to app store
2022-03-04 11:34:41 -07:00
sean-brydon c9a3c5789e Revert "Slack Oauth + verify sig"
This reverts commit ee95795e0f.
2022-03-02 22:01:04 +00:00
sean-brydon ee95795e0f Slack Oauth + verify sig 2022-03-02 21:53:51 +00:00
zomars a369ba895f Revert "Slack Oauth integration - api route ideas"
This reverts commit 35ffa78e92.
2022-03-01 19:39:20 -07:00
zomars f0602df29f Adds location option for daily video 2022-03-01 19:33:19 -07:00
zomars 4f6612f48c Type fixes 2022-03-01 18:17:01 -07:00
zomars 10dde1bbf3 Adds getLocationOptions 2022-03-01 16:17:12 -07:00
Sean Brydon 35ffa78e92 Slack Oauth integration - api route ideas 2022-03-01 22:38:49 +00:00
zomars fd5fd1f9d5 Updated type info 2022-03-01 13:55:49 -07:00
zomars b3f435ec48 Daily fixes 2022-03-01 13:47:09 -07:00
zomars ff1e738cc1 Merge branch 'main' into monorepo/app-store 2022-03-01 11:31:40 -07:00
zomars 579a3af844 WIP 2022-03-01 11:06:34 -07:00
zomars 9c67a7452c WIP 2022-03-01 10:01:57 -07:00
zomars 5519cf36e3 WIP 2022-03-01 09:58:32 -07:00
Leo Giovanetti f0438ed35d
Daily.co app (#2022)
* Daily.co app

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

Co-authored-by: Omar López <zomars@me.com>

* Update packages/app-store/dailyvideo/lib/VideoApiAdapter.ts

Co-authored-by: Omar López <zomars@me.com>

* Missing deps for newly added contants to lib

Co-authored-by: Omar López <zomars@me.com>
2022-03-01 09:53:39 -07:00
zomars 3643d62ce6 Merge branch 'main' into monorepo/app-store 2022-02-28 10:19:52 -07:00
zomars 93a6039c8c Merge branch 'main' into monorepo/app-store 2022-02-23 09:03:57 -07:00
zomars e5c76d10c9 Import fix 2022-02-22 15:11:54 -07:00
zomars 5a63a78660 Renamed zoomvideo app 2022-02-22 15:10:22 -07:00
zomars 5c67a95028 WIP WIP 2022-02-22 15:01:10 -07:00
zomars 505c517237 WIP 2022-02-22 15:01:10 -07:00
zomars e5164fc087 WIP 2022-02-22 15:01:10 -07:00
zomars 12e5c5bf41 WIP 2022-02-22 15:01:10 -07:00
zomars c32c29a624 Moved App definition to types 2022-02-22 15:01:10 -07:00
zomars 3522af1a16 WIP 2022-02-22 15:01:10 -07:00
zomars a7523a7d5d Linting 2022-02-22 15:01:10 -07:00
zomars 8b2fd0d626 Type fixes 2022-02-22 15:01:10 -07:00
zomars 5d2fbe4629 WIP 2022-02-22 15:01:10 -07:00
zomars 73381dbb93 TODO 2022-02-22 15:01:10 -07:00
zomars fa87d34a56 Removes unused file 2022-02-22 15:01:10 -07:00
zomars f4f7024d41 Conflict fixes 2022-02-22 15:01:10 -07:00
zomars 355737a86f WIP 2022-02-22 15:01:10 -07:00
Omar López ed40b09430 Build fixes (#1929)
* Build fixes

* Fixes type error
2022-02-22 15:01:10 -07:00
Juan Esteban Nieto Cifuentes c45da4e3fd Fix URL by removing slash and backslash (#1733)
* Fix URl by removing slash and backslash

* Implement slugify

* Add data type

* Fixing folder structure

* Solve zod-utils conflict
2022-02-22 15:01:10 -07:00
Omar López 66cdc01d0b Upgrade to next 12.1 (#1904)
* Upgrades next to 12.1

* Fixes build

* Updaters e2e test pipelines

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-02-22 14:59:49 -07:00
Omar López 72eb31276c Eslint fixes (#1898)
* Eslint fixes

* Docs build fixes
2022-02-22 14:59:26 -07:00
Omar López b2bc6500a3 Revert "Tweak/gitignore prisma zod (#1905)" (#1906)
This reverts commit 15bfeb30d7.
2022-02-22 14:59:08 -07:00
Omar López 487a4153e4 Tweak/gitignore prisma zod (#1905)
* Extracts ignored createEventTypeBaseInput

* Adds postinstall script
2022-02-22 14:59:08 -07:00
Omar López bab6fcc4ed Revert "Upgrades next to 12.1 (#1895)" (#1903)
This reverts commit ede0e98e1f.
2022-02-22 14:59:08 -07:00
zomars 467d9ad9fd WIP extracting zoom to package 2022-02-22 14:58:25 -07:00
zomars 9fcb82cc80 Updates typings 2022-02-22 14:58:25 -07:00
zomars 6a7fa041f3 Build fixes 2022-02-22 14:58:23 -07:00
zomars f1da729e1a We shouldn't pollute global css 2022-02-22 14:58:23 -07:00
zomars 010d82ec00 patch applied 2022-02-22 14:58:23 -07:00
zomars 801e4c4600 patch applied 2022-02-22 14:58:21 -07:00
180 changed files with 4562 additions and 3031 deletions

229
apps/web/components/App.tsx Normal file
View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -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) {

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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;

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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)}
/>

View File

@ -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)}
/>

View File

@ -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>

View File

@ -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>
);

View File

@ -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";

View File

@ -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";

View File

@ -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",
},
};

View File

@ -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];

View File

@ -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: "",

View File

@ -0,0 +1 @@
export const APPLE_CALENDAR_URL = "https://caldav.icloud.com";

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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>;

View File

@ -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",
};

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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>;

View File

@ -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",
},
];

View File

@ -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);

View File

@ -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}`] });

View File

@ -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>;

View File

@ -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;

View File

@ -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";

View File

@ -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"]) {

View File

@ -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>[] = [];

View File

@ -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];

View File

@ -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];

View File

@ -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";

View File

@ -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);

View File

@ -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;

View File

@ -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";
}

View File

@ -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;

View File

@ -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",
};

View File

@ -1,5 +0,0 @@
import { calendar_v3 } from "googleapis";
export interface ConferenceData {
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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: {

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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

View File

@ -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",

View File

@ -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);

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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")) {

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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";

View File

@ -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(),
},
};
};

View File

@ -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,
},
};
};

View File

@ -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 />

View File

@ -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>
</>
}
/>
);
}

View File

@ -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 sizesfrom
startups to large enterprisesuse Stripe&apos;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.
</>
}
/>
);
}

View File

@ -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.
</>
}
/>
);
}

View File

@ -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}&nbsp;</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

View File

@ -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"));

View File

@ -1,9 +0,0 @@
function RedirectPage() {
return null;
}
export async function getServerSideProps() {
return { redirect: { permanent: false, destination: "/integrations" } };
}
export default RedirectPage;

View File

@ -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", () => {

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -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

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 509 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 981 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More