refactor: Use template literal instead of '+' operator (#11444)

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
pull/11647/head^2
Aldrin 2023-10-04 00:22:19 +05:30 committed by GitHub
parent a186b130cb
commit 3e08c66888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
190 changed files with 478 additions and 483 deletions

View File

@ -94,11 +94,13 @@ To develop locally:
6. Setup Node 6. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project: If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
```sh ```sh
nvm use nvm use
``` ```
You first might need to install the specific version and then use it: You first might need to install the specific version and then use it:
```sh ```sh
nvm install && nvm use nvm install && nvm use
``` ```
@ -134,6 +136,7 @@ yarn test-e2e
``` ```
#### Resolving issues #### Resolving issues
##### E2E test browsers not installed ##### E2E test browsers not installed
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`: Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:

View File

@ -144,17 +144,20 @@ Here is what you need to be able to run Cal.com.
``` ```
4. Set up your `.env` file 4. Set up your `.env` file
- Duplicate `.env.example` to `.env` - Duplicate `.env.example` to `.env`
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file. - Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file. - Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
5. Setup Node 5. Setup Node
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project: If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
```sh ```sh
nvm use nvm use
``` ```
You first might need to install the specific version and then use it: You first might need to install the specific version and then use it:
```sh ```sh
nvm install && nvm use nvm install && nvm use
``` ```
@ -234,6 +237,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
``` ```
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development 1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1" > **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
```sh ```sh
@ -273,6 +277,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
``` ```
#### Resolving issues #### Resolving issues
##### E2E test browsers not installed ##### E2E test browsers not installed
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`: Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:
@ -493,8 +498,7 @@ following
5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs. 5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs.
6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields. 6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields.
7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`. 7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`.
For example, `Cal.com (support@cal.com)`. For example, `Cal.com (support@cal.com)`.
### Obtaining HubSpot Client ID and Secret ### Obtaining HubSpot Client ID and Secret
@ -529,6 +533,7 @@ For example, `Cal.com (support@cal.com)`.
### Obtaining Zoho Calendar Client ID and Secret ### Obtaining Zoho Calendar Client ID and Secret
[Follow these steps](./packages/app-store/zohocalendar/) [Follow these steps](./packages/app-store/zohocalendar/)
### Obtaining Zoho Bigin Client ID and Secret ### Obtaining Zoho Bigin Client ID and Secret
[Follow these steps](./packages/app-store/zoho-bigin/) [Follow these steps](./packages/app-store/zoho-bigin/)

View File

@ -4,9 +4,9 @@ Welcome to the first stage of Cal.ai!
This app lets you chat with your calendar via email: This app lets you chat with your calendar via email:
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?" - Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
- List and rearrange your bookings eg. "Cancel my next meeting" - List and rearrange your bookings eg. "Cancel my next meeting"
- Answer basic questions about your busiest times eg. "How does my Tuesday look?" - Answer basic questions about your busiest times eg. "How does my Tuesday look?"
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings. The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
@ -24,10 +24,10 @@ If you haven't yet, please run the [root setup](/README.md) steps.
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need: Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4 - An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys) - A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
- A default sender email (for example, `ai@cal.dev`) - A default sender email (for example, `ai@cal.dev`)
- The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts)) - The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`. To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.

View File

@ -74,18 +74,19 @@ ${
? `The email references the following @usernames and emails: ${users ? `The email references the following @usernames and emails: ${users
.map( .map(
(u) => (u) =>
(u.id ? `, id: ${u.id}` : "id: (non user)") + `${
(u.username (u.id ? `, id: ${u.id}` : "id: (non user)") +
? u.type === "fromUsername" (u.username
? `, username: @${u.username}` ? u.type === "fromUsername"
: ", username: REDACTED" ? `, username: @${u.username}`
: ", (no username)") + : ", username: REDACTED"
(u.email : ", (no username)") +
? u.type === "fromEmail" (u.email
? `, email: ${u.email}` ? u.type === "fromEmail"
: ", email: REDACTED" ? `, email: ${u.email}`
: ", (no email)") + : ", email: REDACTED"
";" : ", (no email)")
};`
) )
.join("\n")}` .join("\n")}`
: "" : ""

View File

@ -1,3 +1,5 @@
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http"; import { createMocks } from "node-mocks-http";
@ -8,7 +10,6 @@ import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder"; import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
import handler from "../../../pages/api/bookings/_post"; import handler from "../../../pages/api/bookings/_post";
type CustomNextApiRequest = NextApiRequest & Request; type CustomNextApiRequest = NextApiRequest & Request;

View File

@ -265,8 +265,8 @@ export const AppPage = ({
{price !== 0 && ( {price !== 0 && (
<span className="block text-right"> <span className="block text-right">
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar} {feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
{feeType === "monthly" && "/" + t("month")} {feeType === "monthly" && `/${t("month")}`}
</span> </span>
)} )}
@ -286,7 +286,7 @@ export const AppPage = ({
currency: "USD", currency: "USD",
useGrouping: false, useGrouping: false,
}).format(price)} }).format(price)}
{feeType === "monthly" && "/" + t("month")} {feeType === "monthly" && `/${t("month")}`}
</> </>
)} )}
</span> </span>
@ -323,7 +323,7 @@ export const AppPage = ({
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-emphasis font-normal no-underline hover:underline" className="text-emphasis font-normal no-underline hover:underline"
href={"mailto:" + email}> href={`mailto:${email}`}>
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" /> <Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
{email} {email}

View File

@ -130,7 +130,7 @@ function ConnectedCalendarsList(props: Props) {
title={t("something_went_wrong")} title={t("something_went_wrong")}
message={ message={
<span> <span>
<Link href={"/apps/" + item.integration.slug}>{item.integration.name}</Link>:{" "} <Link href={`/apps/${item.integration.slug}`}>{item.integration.name}</Link>:{" "}
{t("calendar_error")} {t("calendar_error")}
</span> </span>
} }

View File

@ -380,7 +380,7 @@ function BookingListItem(booking: BookingItemProps) {
</div> </div>
</Link> </Link>
</td> </td>
<td className={"w-full px-4" + (isRejected ? " line-through" : "")}> <td className={`w-full px-4${isRejected ? " line-through" : ""}`}>
<Link href={bookingLink}> <Link href={bookingLink}>
{/* Time and Badges for mobile */} {/* Time and Badges for mobile */}
<div className="w-full pb-2 pt-4 sm:hidden"> <div className="w-full pb-2 pt-4 sm:hidden">
@ -576,7 +576,7 @@ const FirstAttendee = ({
<a <a
key={user.email} key={user.email}
className=" hover:text-blue-500" className=" hover:text-blue-500"
href={"mailto:" + user.email} href={`mailto:${user.email}`}
onClick={(e) => e.stopPropagation()}> onClick={(e) => e.stopPropagation()}>
{user.name} {user.name}
</a> </a>
@ -590,7 +590,7 @@ type AttendeeProps = {
const Attendee = ({ email, name }: AttendeeProps) => { const Attendee = ({ email, name }: AttendeeProps) => {
return ( return (
<a className="hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}> <a className="hover:text-blue-500" href={`mailto:${email}`} onClick={(e) => e.stopPropagation()}>
{name || email} {name || email}
</a> </a>
); );

View File

@ -121,7 +121,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: `Invalid URL for ${eventLocationType.label}. ${ message: `Invalid URL for ${eventLocationType.label}. ${
sampleUrl ? "Sample URL: " + sampleUrl : "" sampleUrl ? `Sample URL: ${sampleUrl}` : ""
}`, }`,
}); });
} }

View File

@ -67,7 +67,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark")); (!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => { eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name + " input"; bookingFields[name] = `${name} input`;
}); });
const eventNameObject: EventNameObjectType = { const eventNameObject: EventNameObjectType = {

View File

@ -189,7 +189,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: 0, value: 0,
}, },
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({ ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"), label: `minutes ${t("minutes")}`,
value: minutes, value: minutes,
})), })),
]; ];
@ -225,7 +225,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: 0, value: 0,
}, },
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({ ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
label: minutes + " " + t("minutes"), label: `minutes ${t("minutes")}`,
value: minutes, value: minutes,
})), })),
]; ];
@ -272,7 +272,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
value: -1, value: -1,
}, },
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({ ...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
label: minutes + " " + t("minutes"), label: `minutes ${t("minutes")}`,
value: minutes, value: minutes,
})), })),
]; ];

View File

@ -247,7 +247,7 @@ function EventTypeSingleLayout({
return ( return (
<Shell <Shell
backPath="/event-types" backPath="/event-types"
title={eventType.title + " | " + t("event_type")} title={`${eventType.title} | ${t("event_type")}`}
heading={eventType.title} heading={eventType.title}
CTA={ CTA={
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">

View File

@ -22,7 +22,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
return ( return (
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}> <Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer"> <div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
<Avatar size="md" alt={member.name || ""} imageSrc={"/" + member.username + "/avatar.png"} /> <Avatar size="md" alt={member.name || ""} imageSrc={`/${member.username}/avatar.png`} />
<section className="mt-2 line-clamp-4 w-full space-y-1"> <section className="mt-2 line-clamp-4 w-full space-y-1">
<p className="text-default font-medium">{member.name}</p> <p className="text-default font-medium">{member.name}</p>
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal"> <div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">

View File

@ -179,14 +179,14 @@ function getThemeProviderProps({
); );
} }
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : ""; const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : "";
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined; const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
let embedExplicitlySetThemeSuffix = ""; let embedExplicitlySetThemeSuffix = "";
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const embedTheme = window.getEmbedTheme(); const embedTheme = window.getEmbedTheme();
if (embedTheme) { if (embedTheme) {
embedExplicitlySetThemeSuffix = ":" + embedTheme; embedExplicitlySetThemeSuffix = `:${embedTheme}`;
} }
} }

View File

@ -21,7 +21,7 @@ function getCspPolicy(nonce: string) {
script-src ${ script-src ${
IS_PRODUCTION IS_PRODUCTION
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic ? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
"'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:" `'nonce-${nonce}' 'strict-dynamic' 'self' 'unsafe-inline' https:`
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes : // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
"'unsafe-inline' 'unsafe-eval' https: http:" "'unsafe-inline' 'unsafe-eval' https: http:"
}; };

View File

@ -15,14 +15,9 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base"); const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace // Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
const newDestinationUrl = const newDestinationUrl = `${
destinationUrlObj.pathname + destinationUrlObj.pathname
"/embed?" + }/embed?${destinationUrlObj.searchParams.toString()}&layout=${layout}&embed=${embed}`;
destinationUrlObj.searchParams.toString() +
"&layout=" +
layout +
"&embed=" +
embed;
return { return {
...ssrResponse, ...ssrResponse,

View File

@ -21,11 +21,11 @@ process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
// So we can test deploy previews preview // So we can test deploy previews preview
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) { if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL; process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`;
} }
// Check for configuration of NEXTAUTH_URL before overriding // Check for configuration of NEXTAUTH_URL before overriding
if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) { if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) {
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_WEBAPP_URL + "/api/auth"; process.env.NEXTAUTH_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/auth`;
} }
if (!process.env.NEXT_PUBLIC_WEBSITE_URL) { if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL; process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;

View File

@ -125,7 +125,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
{user.away ? ( {user.away ? (
<div className="overflow-hidden rounded-sm border "> <div className="overflow-hidden rounded-sm border ">
<div className="text-muted p-8 text-center"> <div className="text-muted p-8 text-center">
<h2 className="font-cal text-default mb-2 text-3xl">😴{" " + t("user_away")}</h2> <h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p> <p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
</div> </div>
</div> </div>

View File

@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Google client_secret missing." }); return res.status(400).json({ message: "Google client_secret missing." });
// use differnt callback to normal calendar connection // use differnt callback to normal calendar connection
const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const authUrl = oAuth2Client.generateAuthUrl({ const authUrl = oAuth2Client.generateAuthUrl({

View File

@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_secret || typeof client_secret !== "string") if (!client_secret || typeof client_secret !== "string")
return res.status(400).json({ message: "Google client_secret missing." }); return res.status(400).json({ message: "Google client_secret missing." });
const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback"; const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
if (!code) { if (!code) {
@ -54,11 +54,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
if (!teamId) { if (!teamId) {
res.redirect(getSafeRedirectUrl(WEBAPP_URL + "/settings") ?? `${WEBAPP_URL}/teams`); res.redirect(getSafeRedirectUrl(`${WEBAPP_URL}/settings`) ?? `${WEBAPP_URL}/teams`);
} }
res.redirect( res.redirect(
getSafeRedirectUrl(WEBAPP_URL + `/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ?? getSafeRedirectUrl(`${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ??
`${WEBAPP_URL}/teams` `${WEBAPP_URL}/teams`
); );
} }

View File

@ -31,7 +31,7 @@ export default function Apps({ categories }: inferSSRProps<typeof getServerSideP
{categories.map((category) => ( {categories.map((category) => (
<Link <Link
key={category.name} key={category.name}
href={"/apps/categories/" + category.name} href={`/apps/categories/${category.name}`}
data-testid={`app-store-category-${category.name}`} data-testid={`app-store-category-${category.name}`}
className="bg-subtle relative flex rounded-sm px-6 py-4 sm:block"> className="bg-subtle relative flex rounded-sm px-6 py-4 sm:block">
<div className="self-center"> <div className="self-center">

View File

@ -30,12 +30,12 @@ export default function Provider(props: SSOProviderPageProps) {
const email = searchParams?.get("email"); const email = searchParams?.get("email");
if (!email) { if (!email) {
router.push("/auth/error?error=" + "Email not provided"); router.push(`/auth/error?error=Email not provided`);
return; return;
} }
if (!props.isSAMLLoginEnabled) { if (!props.isSAMLLoginEnabled) {
router.push("/auth/error?error=" + "SAML login not enabled"); router.push(`/auth/error?error=SAML login not enabled`);
return; return;
} }
@ -56,7 +56,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const providerParam = asStringOrNull(context.query.provider); const providerParam = asStringOrNull(context.query.provider);
const emailParam = asStringOrNull(context.query.email); const emailParam = asStringOrNull(context.query.email);
const usernameParam = asStringOrNull(context.query.username); const usernameParam = asStringOrNull(context.query.username);
const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : ""); const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`;
if (!providerParam) { if (!providerParam) {
throw new Error(`File is not named sso/[provider]`); throw new Error(`File is not named sso/[provider]`);
} }
@ -120,7 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (error) { if (error) {
return { return {
redirect: { redirect: {
destination: "/auth/error?error=" + error, destination: `/auth/error?error=${error}`,
permanent: false, permanent: false,
}, },
}; };

View File

@ -120,7 +120,7 @@ export default function Verify() {
? "Your payment failed" ? "Your payment failed"
: sessionId : sessionId
? "Payment successful!" ? "Payment successful!"
: "Verify your email" + " | " + APP_NAME} : `Verify your email | ${APP_NAME}`}
</title> </title>
</Head> </Head>
<div className="flex min-h-screen flex-col items-center justify-center px-6"> <div className="flex min-h-screen flex-col items-center justify-center px-6">

View File

@ -151,7 +151,7 @@ export default function Availability() {
return ( return (
<Shell <Shell
backPath={fromEventType ? true : "/availability"} backPath={fromEventType ? true : "/availability"}
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")} title={schedule?.name ? `${schedule.name} | ${t("availability")}` : t("availability")}
heading={ heading={
<Controller <Controller
control={form.control} control={form.control}

View File

@ -133,7 +133,7 @@ Troubleshoot.PageWrapper = PageWrapper;
function convertMinsToHrsMins(mins: number) { function convertMinsToHrsMins(mins: number) {
const h = Math.floor(mins / 60); const h = Math.floor(mins / 60);
const m = mins % 60; const m = mins % 60;
const hs = h < 10 ? "0" + h : h; const hs = h < 10 ? `0${h}` : h;
const ms = m < 10 ? "0" + m : m; const ms = m < 10 ? `0${m}` : m;
return `${hs}:${ms}`; return `${hs}:${ms}`;
} }

View File

@ -269,13 +269,13 @@ export default function Success(props: SuccessProps) {
} }
if (needsConfirmation) { if (needsConfirmation) {
if (props.profile.name !== null) { if (props.profile.name !== null) {
return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, { return t(`user_needs_to_confirm_or_reject_booking${titleSuffix}`, {
user: props.profile.name, user: props.profile.name,
}); });
} }
return t("needs_to_be_confirmed_or_rejected" + titleSuffix); return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
} }
return t("emailed_you_and_attendees" + titleSuffix); return t(`emailed_you_and_attendees${titleSuffix}`);
} }
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app // This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
@ -592,23 +592,24 @@ export default function Success(props: SuccessProps) {
</span> </span>
<div className="justify-left mt-1 flex text-left sm:mt-0"> <div className="justify-left mt-1 flex text-left sm:mt-0">
<Link <Link
href={ href={`https://calendar.google.com/calendar/r/eventedit?dates=${date
`https://calendar.google.com/calendar/r/eventedit?dates=${date .utc()
.utc() .format("YYYYMMDDTHHmmss[Z]")}/${date
.format("YYYYMMDDTHHmmss[Z]")}/${date .add(calculatedDuration, "minute")
.add(calculatedDuration, "minute") .utc()
.utc() .format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${ props.eventType.description
props.eventType.description }${
}` + typeof locationVideoCallUrl === "string"
(typeof locationVideoCallUrl === "string" ? `&location=${encodeURIComponent(locationVideoCallUrl)}`
? "&location=" + encodeURIComponent(locationVideoCallUrl) : ""
: "") + }${
(props.eventType.recurringEvent props.eventType.recurringEvent
? "&recur=" + ? `&recur=${encodeURIComponent(
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString()) new RRule(props.eventType.recurringEvent).toString()
: "") )}`
} : ""
}`}
className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2"> className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2">
<svg <svg
className="-mt-1.5 inline-block h-4 w-4" className="-mt-1.5 inline-block h-4 w-4"
@ -622,17 +623,17 @@ export default function Success(props: SuccessProps) {
<Link <Link
href={ href={
encodeURI( encodeURI(
"https://outlook.live.com/calendar/0/deeplink/compose?body=" + `https://outlook.live.com/calendar/0/deeplink/compose?body=${
props.eventType.description + props.eventType.description
"&enddt=" + }&enddt=${date
date.add(calculatedDuration, "minute").utc().format() + .add(calculatedDuration, "minute")
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + .utc()
date.utc().format() + .format()}&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=${date
"&subject=" + .utc()
eventName .format()}&subject=${eventName}`
) + ) +
(locationVideoCallUrl (locationVideoCallUrl
? "&location=" + encodeURIComponent(locationVideoCallUrl) ? `&location=${encodeURIComponent(locationVideoCallUrl)}`
: "") : "")
} }
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2" className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
@ -649,17 +650,17 @@ export default function Success(props: SuccessProps) {
<Link <Link
href={ href={
encodeURI( encodeURI(
"https://outlook.office.com/calendar/0/deeplink/compose?body=" + `https://outlook.office.com/calendar/0/deeplink/compose?body=${
props.eventType.description + props.eventType.description
"&enddt=" + }&enddt=${date
date.add(calculatedDuration, "minute").utc().format() + .add(calculatedDuration, "minute")
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" + .utc()
date.utc().format() + .format()}&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=${date
"&subject=" + .utc()
eventName .format()}&subject=${eventName}`
) + ) +
(locationVideoCallUrl (locationVideoCallUrl
? "&location=" + encodeURIComponent(locationVideoCallUrl) ? `&location=${encodeURIComponent(locationVideoCallUrl)}`
: "") : "")
} }
className="text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2" className="text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2"
@ -674,9 +675,9 @@ export default function Success(props: SuccessProps) {
</svg> </svg>
</Link> </Link>
<Link <Link
href={"data:text/calendar," + eventLink()} href={`data:text/calendar,${eventLink()}`}
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2" className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
download={props.eventType.title + ".ics"}> download={`${props.eventType.title}.ics`}>
<svg <svg
version="1.1" version="1.1"
fill="currentColor" fill="currentColor"

View File

@ -140,13 +140,13 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
<div> <div>
<span <span
className="text-default font-semibold ltr:mr-1 rtl:ml-1" className="text-default font-semibold ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}> data-testid={`event-type-title-${type.id}`}>
{type.title} {type.title}
</span> </span>
{group.profile.slug ? ( {group.profile.slug ? (
<small <small
className="text-subtle hidden font-normal leading-4 sm:inline" className="text-subtle hidden font-normal leading-4 sm:inline"
data-testid={"event-type-slug-" + type.id}> data-testid={`event-type-slug-${type.id}`}>
{`/${ {`/${
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder") type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
}/${type.slug}`} }/${type.slug}`}
@ -177,13 +177,13 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
<div> <div>
<span <span
className="text-default font-semibold ltr:mr-1 rtl:ml-1" className="text-default font-semibold ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}> data-testid={`event-type-title-${type.id}`}>
{type.title} {type.title}
</span> </span>
{group.profile.slug ? ( {group.profile.slug ? (
<small <small
className="text-subtle hidden font-normal leading-4 sm:inline" className="text-subtle hidden font-normal leading-4 sm:inline"
data-testid={"event-type-slug-" + type.id}> data-testid={`event-type-slug-${type.id}`}>
{`/${group.profile.slug}/${type.slug}`} {`/${group.profile.slug}/${type.slug}`}
</small> </small>
) : null} ) : null}
@ -479,7 +479,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</> </>
)} )}
<Dropdown modal={false}> <Dropdown modal={false}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}> <DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
<Button <Button
type="button" type="button"
variant="icon" variant="icon"
@ -493,9 +493,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuItem> <DropdownMenuItem>
<DropdownItem <DropdownItem
type="button" type="button"
data-testid={"event-type-edit-" + type.id} data-testid={`event-type-edit-${type.id}`}
StartIcon={Edit2} StartIcon={Edit2}
onClick={() => router.push("/event-types/" + type.id)}> onClick={() => router.push(`/event-types/${type.id}`)}>
{t("edit")} {t("edit")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>
@ -505,7 +505,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<DropdownItem <DropdownItem
type="button" type="button"
data-testid={"event-type-duplicate-" + type.id} data-testid={`event-type-duplicate-${type.id}`}
StartIcon={Copy} StartIcon={Copy}
onClick={() => openDuplicateModal(type, group)}> onClick={() => openDuplicateModal(type, group)}>
{t("duplicate")} {t("duplicate")}
@ -555,7 +555,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</div> </div>
<div className="min-w-9 mx-5 flex sm:hidden"> <div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown> <Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}> <DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} /> <Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuPortal> <DropdownMenuPortal>
@ -573,7 +573,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<DropdownItem <DropdownItem
data-testid={"event-type-duplicate-" + type.id} data-testid={`event-type-duplicate-${type.id}`}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(calLink); navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success"); showToast(t("link_copied"), "success");
@ -588,7 +588,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{isNativeShare ? ( {isNativeShare ? (
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<DropdownItem <DropdownItem
data-testid={"event-type-duplicate-" + type.id} data-testid={`event-type-duplicate-${type.id}`}
onClick={() => { onClick={() => {
navigator navigator
.share({ .share({
@ -608,7 +608,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{!readOnly && ( {!readOnly && (
<DropdownMenuItem className="outline-none"> <DropdownMenuItem className="outline-none">
<DropdownItem <DropdownItem
onClick={() => router.push("/event-types/" + type.id)} onClick={() => router.push(`/event-types/${type.id}`)}
StartIcon={Edit} StartIcon={Edit}
className="w-full rounded-none"> className="w-full rounded-none">
{t("edit")} {t("edit")}
@ -620,7 +620,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownItem <DropdownItem
onClick={() => openDuplicateModal(type, group)} onClick={() => openDuplicateModal(type, group)}
StartIcon={Copy} StartIcon={Copy}
data-testid={"event-type-duplicate-" + type.id}> data-testid={`event-type-duplicate-${type.id}`}>
{t("duplicate")} {t("duplicate")}
</DropdownItem> </DropdownItem>
</DropdownMenuItem> </DropdownMenuItem>

View File

@ -63,14 +63,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef); const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
const eventPage = const eventPage = `${
(eventType.team eventType.team
? "team/" + eventType.team.slug ? `team/${eventType.team.slug}`
: dynamicEventSlugRef : dynamicEventSlugRef
? booking.dynamicGroupSlugRef ? booking.dynamicGroupSlugRef
: booking.user?.username || "rick") /* This shouldn't happen */ + : booking.user?.username || "rick" /* This shouldn't happen */
"/" + }/${eventType?.slug}`;
eventType?.slug;
const destinationUrl = new URLSearchParams(); const destinationUrl = new URLSearchParams();
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId); destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);

View File

@ -190,7 +190,7 @@ const CalendarsView = () => {
} }
<div className="flex-grow truncate pl-2"> <div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse"> <ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
<Link href={"/apps/" + item.integration.slug}> <Link href={`/apps/${item.integration.slug}`}>
{item.integration.name || item.integration.title} {item.integration.name || item.integration.title}
</Link> </Link>
{data?.destinationCalendar?.credentialId === item.credentialId && ( {data?.destinationCalendar?.credentialId === item.credentialId && (

View File

@ -281,7 +281,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
return { return {
redirect: { redirect: {
permanent: false, permanent: false,
destination: "/auth/login?callbackUrl=" + `${WEBAPP_URL}/${ctx.query.callbackUrl}`, destination: `/auth/login?callbackUrl=${WEBAPP_URL}/${ctx.query.callbackUrl}`,
}, },
}; };
} }

View File

@ -102,7 +102,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
items={type.users.map((user) => ({ items={type.users.map((user) => ({
alt: user.name || "", alt: user.name || "",
title: user.name || "", title: user.name || "",
image: "/" + user.username + "/avatar.png" || "", image: `/${user.username}/avatar.png` || "",
}))} }))}
/> />
</div> </div>
@ -160,7 +160,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<div className="space-y-6" data-testid="event-types"> <div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900"> <div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted p-8 text-center"> <div className="text-muted p-8 text-center">
<h2 className="font-cal text-emphasis mb-2 text-3xl">{" " + t("org_no_teams_yet")}</h2> <h2 className="font-cal text-emphasis mb-2 text-3xl">{` ${t("org_no_teams_yet")}`}</h2>
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p> <p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
</div> </div>
</div> </div>
@ -324,7 +324,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
...type, ...type,
users: type.users.map((user) => ({ users: type.users.map((user) => ({
...user, ...user,
avatar: "/" + user.username + "/avatar.png", avatar: `/${user.username}/avatar.png`,
})), })),
descriptionAsSafeHTML: markdownToSafeHTML(type.description), descriptionAsSafeHTML: markdownToSafeHTML(type.description),
})) ?? null; })) ?? null;

View File

@ -95,12 +95,12 @@ export default function JoinCall(props: JoinCallPageProps) {
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} /> <meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content={`${WEBSITE_URL}/video`} /> <meta property="og:url" content={`${WEBSITE_URL}/video`} />
<meta property="og:title" content={APP_NAME + " Video"} /> <meta property="og:title" content={`${APP_NAME} Video`} />
<meta property="og:description" content={t("quick_video_meeting")} /> <meta property="og:description" content={t("quick_video_meeting")} />
<meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} /> <meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} />
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={`${WEBSITE_URL}/video`} /> <meta property="twitter:url" content={`${WEBSITE_URL}/video`} />
<meta property="twitter:title" content={APP_NAME + " Video"} /> <meta property="twitter:title" content={`${APP_NAME} Video`} />
<meta property="twitter:description" content={t("quick_video_meeting")} /> <meta property="twitter:description" content={t("quick_video_meeting")} />
</Head> </Head>
<div style={{ zIndex: 2, position: "relative" }}> <div style={{ zIndex: 2, position: "relative" }}>

View File

@ -44,7 +44,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
</h2> </h2>
<p className="text-subtle text-center"> <p className="text-subtle text-center">
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" /> <Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")} {dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
</p> </p>
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2> <h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
<p className="text-subtle text-center"> <p className="text-subtle text-center">
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" /> <Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")} {dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
</p> </p>
</> </>
} }

View File

@ -23,7 +23,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked // book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp( let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`
)); ));
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`; exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;

View File

@ -28,7 +28,7 @@ export const createPaymentsFixture = (page: Page) => {
}, },
}, },
data: {}, data: {},
externalId: "DEMO_PAYMENT_FROM_DB_" + Date.now(), externalId: `DEMO_PAYMENT_FROM_DB_${Date.now()}`,
booking: { booking: {
connect: { connect: {
id: bookingId, id: bookingId,

View File

@ -23,7 +23,7 @@ export const nextServer = async ({ port = 3000 } = { port: 3000 }) => {
process.env.PLAYWRIGHT_TEST_BASE_URL = process.env.PLAYWRIGHT_TEST_BASE_URL =
process.env.NEXT_PUBLIC_WEBAPP_URL = process.env.NEXT_PUBLIC_WEBAPP_URL =
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBSITE_URL =
"http://localhost:" + port; `http://localhost:${port}`;
const app = next({ const app = next({
dev: dev, dev: dev,
port, port,
@ -46,7 +46,7 @@ export const nextServer = async ({ port = 3000 } = { port: 3000 }) => {
resolve(server); resolve(server);
}); });
server.on("error", (error) => { server.on("error", (error) => {
if (error) throw new Error("Could not start Next.js server -" + error.message); if (error) throw new Error(`Could not start Next.js server - ${error.message}`);
}); });
}); });
return server; return server;

View File

@ -65,7 +65,7 @@ test.describe("OAuth Provider", () => {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + tokenData.access_token, Authorization: `Bearer ${tokenData.access_token}`,
}, },
}); });
@ -96,7 +96,7 @@ test.describe("OAuth Provider", () => {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + tokenData.access_token, Authorization: `Bearer ${tokenData.access_token}`,
}, },
}); });
@ -150,7 +150,7 @@ test.describe("OAuth Provider", () => {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + tokenData.access_token, Authorization: `Bearer ${tokenData.access_token}`,
}, },
}); });
@ -181,7 +181,7 @@ test.describe("OAuth Provider", () => {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + tokenData.access_token, Authorization: `Bearer ${tokenData.access_token}`,
}, },
}); });

View File

@ -404,8 +404,8 @@ export const getDate = (
year = year + 1; year = year + 1;
} }
const date = _date < 10 ? "0" + _date : _date; const date = _date < 10 ? `0${_date}` : _date;
const month = _month < 10 ? "0" + _month : _month; const month = _month < 10 ? `0${_month}` : _month;
return { return {
date, date,

View File

@ -208,7 +208,7 @@ export const AppForm = ({
</Text> </Text>
<Text> <Text>
Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "} Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "}
{getAppDirPath(slug, isTemplate) + "/static/icon.svg"} {`${getAppDirPath(slug, isTemplate)}/static/icon.svg`}
</Text> </Text>
<Newline /> <Newline />
<Text bold underline color="blue"> <Text bold underline color="blue">

View File

@ -12,7 +12,7 @@ export function Message({
if (message.showInProgressIndicator) { if (message.showInProgressIndicator) {
const interval = setInterval(() => { const interval = setInterval(() => {
setProgressText((progressText) => { setProgressText((progressText) => {
return progressText.length > 3 ? "" : progressText + "."; return progressText.length > 3 ? "" : `${progressText}.`;
}); });
}, 1000); }, 1000);
return () => { return () => {

View File

@ -1,7 +1,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { APP_STORE_PATH, TEMPLATES_PATH, IS_WINDOWS_PLATFORM} from "./constants"; import { APP_STORE_PATH, TEMPLATES_PATH, IS_WINDOWS_PLATFORM } from "./constants";
import execSync from "./utils/execSync"; import execSync from "./utils/execSync";
const slugify = (str: string) => { const slugify = (str: string) => {
@ -70,7 +70,11 @@ export const BaseAppFork = {
const appDirPath = getAppDirPath(slug, isTemplate); const appDirPath = getAppDirPath(slug, isTemplate);
if (!editMode) { if (!editMode) {
await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`); await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`);
await execSync(IS_WINDOWS_PLATFORM ? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i` : `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`); await execSync(
IS_WINDOWS_PLATFORM
? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i`
: `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`
);
} else { } else {
if (!oldSlug) { if (!oldSlug) {
throw new Error("oldSlug is required when editMode is true"); throw new Error("oldSlug is required when editMode is true");
@ -79,7 +83,9 @@ export const BaseAppFork = {
// We need to rename only if they are different // We need to rename only if they are different
const oldAppDirPath = getAppDirPath(oldSlug, isTemplate); const oldAppDirPath = getAppDirPath(oldSlug, isTemplate);
await execSync(IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`); await execSync(
IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`
);
} }
} }
updatePackageJson({ slug, appDirPath, appDescription: description }); updatePackageJson({ slug, appDirPath, appDescription: description });

View File

@ -1,11 +1,13 @@
## App Contribution Guidelines ## App Contribution Guidelines
#### `DESCRIPTION.md` #### `DESCRIPTION.md`
1. images - include atleast 4 images (do we have a recommended size here?). Can show app in use and/or installation steps 1. images - include atleast 4 images (do we have a recommended size here?). Can show app in use and/or installation steps
2. add only file name for images, path not required. i.e. `1.jpeg`, not `/app-store/zohocalendar/1.jpeg` 2. add only file name for images, path not required. i.e. `1.jpeg`, not `/app-store/zohocalendar/1.jpeg`
3. description should include what the integration with Cal allows the user to do e.g. `Allows you to sync Cal bookings with your Zoho Calendar` 3. description should include what the integration with Cal allows the user to do e.g. `Allows you to sync Cal bookings with your Zoho Calendar`
#### `README.md` #### `README.md`
1. Include installation instructions and links to the app's website. 1. Include installation instructions and links to the app's website.
2. For url use `<baseUrl>/api/integrations`, rather than `<Cal.com>/api/integrations` 2. For url use `<baseUrl>/api/integrations`, rather than `<Cal.com>/api/integrations`
@ -16,6 +18,7 @@
2. description here should not exceed 10 words (this is arbitrary, but should not be long otherwise it's truncated in the app store) 2. description here should not exceed 10 words (this is arbitrary, but should not be long otherwise it's truncated in the app store)
#### Others #### Others
1. Add API documentation links in comments for files `api`, `lib` and `types` 1. Add API documentation links in comments for files `api`, `lib` and `types`
2. Use [`AppDeclarativeHandler`](../types/AppHandler.d.ts) across all apps. Whatever isn't supported in it, support that. 2. Use [`AppDeclarativeHandler`](../types/AppHandler.d.ts) across all apps. Whatever isn't supported in it, support that.
3. README should be added in the respective app and can be linked in main README [like this](https://github.com/calcom/cal.com/pull/10429/files/155ac84537d12026f595551fe3542e810b029714#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R509) 3. README should be added in the respective app and can be linked in main README [like this](https://github.com/calcom/cal.com/pull/10429/files/155ac84537d12026f595551fe3542e810b029714#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R509)

View File

@ -44,7 +44,7 @@ export default function AppCard({
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0"> <div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */} {/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
<Link <Link
href={"/apps/" + app.slug} href={`/apps/${app.slug}`}
className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}> className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}>
<img <img
className={classNames( className={classNames(

View File

@ -64,7 +64,7 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
const stateStr = encodeURIComponent(JSON.stringify(state)); const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`; const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
const res = await fetch(`/api/integrations/${type}/add` + searchParams); const res = await fetch(`/api/integrations/${type}/add${searchParams}`);
if (!res.ok) { if (!res.ok) {
const errorBody = await res.json(); const errorBody = await res.json();

View File

@ -24,9 +24,7 @@ async function handler(req: NextApiRequest) {
client_id, client_id,
}; };
const query = stringify(params); const query = stringify(params);
const url = `https://launchpad.37signals.com/authorization/new?${query}&redirect_uri=${ const url = `https://launchpad.37signals.com/authorization/new?${query}&redirect_uri=${WEBAPP_URL}/api/integrations/basecamp3/callback`;
WEBAPP_URL + "/api/integrations/basecamp3/callback"
}`;
return { url }; return { url };
} }

View File

@ -11,7 +11,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code } = req.query; const { code } = req.query;
const { client_id, client_secret, user_agent } = await getAppKeysFromSlug("basecamp3"); const { client_id, client_secret, user_agent } = await getAppKeysFromSlug("basecamp3");
const redirectUri = WEBAPP_URL + "/api/integrations/basecamp3/callback"; const redirectUri = `${WEBAPP_URL}/api/integrations/basecamp3/callback`;
const params = new URLSearchParams({ const params = new URLSearchParams({
type: "web_server", type: "web_server",

View File

@ -105,14 +105,9 @@ export default class BasecampCalendarService implements Calendar {
minute: "numeric", minute: "numeric",
}); });
const baseString = `<div>Event title: ${event.title}<br/>Date and time: ${date}, ${startTime} - ${endTime} ${timeZone}<br/>View on Cal.com: <a target="_blank" rel="noreferrer" class="autolinked" data-behavior="truncate" href="https://app.cal.com/booking/${event.uid}">https://app.cal.com/booking/${event.uid}</a> `; const baseString = `<div>Event title: ${event.title}<br/>Date and time: ${date}, ${startTime} - ${endTime} ${timeZone}<br/>View on Cal.com: <a target="_blank" rel="noreferrer" class="autolinked" data-behavior="truncate" href="https://app.cal.com/booking/${event.uid}">https://app.cal.com/booking/${event.uid}</a> `;
const guestString = const guestString = `<br/>Guests: ${event.attendees.reduce((acc, attendee) => {
"<br/>Guests: " + return `${acc}<br/><a target=\"_blank\" rel=\"noreferrer\" class=\"autolinked\" data-behavior=\"truncate\" href=\"mailto:${attendee.email}\">${attendee.email}</a>`;
event.attendees.reduce((acc, attendee) => { }, "")}`;
return (
acc +
`<br/><a target=\"_blank\" rel=\"noreferrer\" class=\"autolinked\" data-behavior=\"truncate\" href=\"mailto:${attendee.email}\">${attendee.email}</a>`
);
}, "");
const videoString = event.videoCallData const videoString = event.videoCallData
? `<br/>Join on video: ${event.videoCallData.url}</div>` ? `<br/>Join on video: ${event.videoCallData.url}</div>`

View File

@ -19,4 +19,3 @@ Every CalDav Provider is different and needs minor adjustments.
Follow our CalDav Roadmap here: <a class="text-blue-500" target="_blank" href="https://github.com/calcom/cal.com/issues/3457">https://github.com/calcom/cal.com/issues/3457</a>. Follow our CalDav Roadmap here: <a class="text-blue-500" target="_blank" href="https://github.com/calcom/cal.com/issues/3457">https://github.com/calcom/cal.com/issues/3457</a>.
If your CalDav connection is not working, please try another Calendar provider (Google Mail or another CalDav). If your CalDav connection is not working, please try another Calendar provider (Google Mail or another CalDav).

View File

@ -49,12 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
let message = e.message; let message = e.message;
if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) { if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
const adminUrl = const adminUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}${
parsedUrl.protocol + parsedUrl.port ? `:${parsedUrl.port}` : ""
"//" + }/admin/?/settings/standard/`;
parsedUrl.hostname +
(parsedUrl.port ? ":" + parsedUrl.port : "") +
"/admin/?/settings/standard/";
message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`; message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`;
return res.status(500).json({ message, actionUrl: adminUrl }); return res.status(500).json({ message, actionUrl: adminUrl });
} }

View File

@ -71,7 +71,7 @@ export const fetcher = async (endpoint: string, init?: RequestInit | undefined)
return fetch(`https://api.daily.co/v1${endpoint}`, { return fetch(`https://api.daily.co/v1${endpoint}`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: "Bearer " + api_key, Authorization: `Bearer ${api_key}`,
"Content-Type": "application/json", "Content-Type": "application/json",
...init?.headers, ...init?.headers,
}, },

View File

@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret; if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
if (!client_id) return res.status(400).json({ message: "Google client_id missing." }); if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." }); if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
const redirect_uri = WEBAPP_URL_FOR_OAUTH + "/api/integrations/googlecalendar/callback"; const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
const authUrl = oAuth2Client.generateAuthUrl({ const authUrl = oAuth2Client.generateAuthUrl({

View File

@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "Google client_id missing." }); if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." }); if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
const redirect_uri = WEBAPP_URL_FOR_OAUTH + "/api/integrations/googlecalendar/callback"; const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri); const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
res.redirect( res.redirect(
getSafeRedirectUrl(CAL_URL + "/apps/installed/conferencing?hl=google-meet") ?? getSafeRedirectUrl(`${CAL_URL}/apps/installed/conferencing?hl=google-meet`) ??
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" }) getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
); );
} }

View File

@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id; if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." }); if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
const redirectUri = WEBAPP_URL + "/api/integrations/hubspot/callback"; const redirectUri = `${WEBAPP_URL}/api/integrations/hubspot/callback`;
const url = hubspotClient.oauth.getAuthorizationUrl( const url = hubspotClient.oauth.getAuthorizationUrl(
client_id, client_id,
redirectUri, redirectUri,

View File

@ -39,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken( const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
"authorization_code", "authorization_code",
code, code,
WEBAPP_URL + "/api/integrations/hubspot/callback", `${WEBAPP_URL}/api/integrations/hubspot/callback`,
client_id, client_id,
client_secret client_secret
); );

View File

@ -179,7 +179,7 @@ export default class HubspotCalendarService implements Calendar {
await hubspotClient.oauth.tokensApi.createToken( await hubspotClient.oauth.tokensApi.createToken(
"refresh_token", "refresh_token",
undefined, undefined,
WEBAPP_URL + "/api/integrations/hubspot/callback", `${WEBAPP_URL}/api/integrations/hubspot/callback`,
this.client_id, this.client_id,
this.client_secret, this.client_secret,
refreshToken refreshToken

View File

@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const params = { const params = {
client_id, client_id,
redirect_uri: WEBAPP_URL_FOR_OAUTH + "/api/integrations/intercom/callback", redirect_uri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/intercom/callback`,
state, state,
response_type: "code", response_type: "code",
}; };

View File

@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (response.status !== 200) { if (response.status !== 200) {
log.error("get user_access_token failed", responseBody); log.error("get user_access_token failed", responseBody);
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody)); return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
} }
// Find the admin id from the accompte thanks to access_token and store it // Find the admin id from the accompte thanks to access_token and store it
@ -64,7 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (admin.status !== 200) { if (admin.status !== 200) {
log.error("get admin_id failed", adminBody); log.error("get admin_id failed", adminBody);
return res.redirect("/apps/installed?error=" + JSON.stringify(adminBody)); return res.redirect(`/apps/installed?error=${JSON.stringify(adminBody)}`);
} }
const adminId = adminBody.id; const adminId = adminBody.id;
@ -86,7 +86,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
); );
res.redirect( res.redirect(
getSafeRedirectUrl(CAL_URL + "/apps/installed/automation?hl=intercom") ?? getSafeRedirectUrl(`${CAL_URL}/apps/installed/automation?hl=intercom`) ??
getInstalledAppPath({ variant: "automation", slug: "intercom" }) getInstalledAppPath({ variant: "automation", slug: "intercom" })
); );
} }

View File

@ -6,9 +6,7 @@
"logo": "icon.svg", "logo": "icon.svg",
"url": "https://github.com/vachmara", "url": "https://github.com/vachmara",
"variant": "automation", "variant": "automation",
"categories": [ "categories": ["automation"],
"automation"
],
"publisher": "Valentin Chmara", "publisher": "Valentin Chmara",
"email": "valentinchmara@gmail.com", "email": "valentinchmara@gmail.com",
"description": "Enhance your scheduling and appointment management experience with the Intercom Integration for Cal.com.", "description": "Enhance your scheduling and appointment management experience with the Intercom Integration for Cal.com.",

View File

@ -33,7 +33,7 @@ const JitsiVideoApiAdapter = (): VideoApiAdapter => {
type: metadata.type, type: metadata.type,
id: meetingID, id: meetingID,
password: "", password: "",
url: hostUrl + "/" + encodeURIComponent(meetingID), url: `${hostUrl}/${encodeURIComponent(meetingID)}`,
}); });
}, },
deleteMeeting: async (): Promise<void> => { deleteMeeting: async (): Promise<void> => {

View File

@ -22,7 +22,7 @@ async function getHandler(req: NextApiRequest) {
const params = { const params = {
app_id, app_id,
redirect_uri: WEBAPP_URL + "/api/integrations/larkcalendar/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/larkcalendar/callback`,
state, state,
}; };

View File

@ -28,7 +28,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const response = await fetch(`https://${LARK_HOST}/open-apis/authen/v1/access_token`, { const response = await fetch(`https://${LARK_HOST}/open-apis/authen/v1/access_token`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer " + appAccessToken, Authorization: `Bearer ${appAccessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
@ -41,7 +41,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
if (!response.ok || responseBody.code !== 0) { if (!response.ok || responseBody.code !== 0) {
log.error("get user_access_token failed with none 0 code", responseBody); log.error("get user_access_token failed with none 0 code", responseBody);
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody)); return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
} }
const key: LarkAuthCredentials = { const key: LarkAuthCredentials = {

View File

@ -114,7 +114,7 @@ export async function sendPostMsg(
const response = await fetch(`https://${LARK_HOST}/open-apis/im/v1/messages?receive_id_type=open_id`, { const response = await fetch(`https://${LARK_HOST}/open-apis/im/v1/messages?receive_id_type=open_id`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer " + tenantAccessToken, Authorization: `Bearer ${tenantAccessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -122,7 +122,7 @@ export default class LarkCalendarService implements Calendar {
return fetch(`${this.url}${endpoint}`, { return fetch(`${this.url}${endpoint}`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
...init?.headers, ...init?.headers,
}, },

View File

@ -402,8 +402,9 @@ export function getSuccessPageLocationMessage(
if (bookingStatus === BookingStatus.CANCELLED || bookingStatus === BookingStatus.REJECTED) { if (bookingStatus === BookingStatus.CANCELLED || bookingStatus === BookingStatus.REJECTED) {
locationToDisplay == t("web_conference"); locationToDisplay == t("web_conference");
} else if (isConfirmed) { } else if (isConfirmed) {
locationToDisplay = locationToDisplay = `${getHumanReadableLocationValue(location, t)}: ${t(
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_confirmation_email"); "meeting_url_in_confirmation_email"
)}`;
} else { } else {
locationToDisplay = t("web_conferencing_details_to_follow"); locationToDisplay = t("web_conferencing_details_to_follow");
} }

View File

@ -1,6 +1,5 @@
# Setting up Make Integration # Setting up Make Integration
1. Install the app from the Cal app store and generate an API key. Copy the API key. 1. Install the app from the Cal app store and generate an API key. Copy the API key.
2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app. 2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app.
3. Create a [Make account](https://www.make.com/en/login), if you don't have one. 3. Create a [Make account](https://www.make.com/en/login), if you don't have one.

View File

@ -22,7 +22,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return res.status(500).json({ message: "Unable to get bookings." }); return res.status(500).json({ message: "Unable to get bookings." });
} }
if (bookings.length === 0) { if (bookings.length === 0) {
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId; const requested = validKey.teamId ? `teamId: ${validKey.teamId}` : `userId: ${validKey.userId}`;
return res.status(404).json({ return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``, message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
}); });

View File

@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
response_type: "code", response_type: "code",
scope: scopes.join(" "), scope: scopes.join(" "),
client_id, client_id,
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`,
state, state,
}; };
const query = stringify(params); const query = stringify(params);

View File

@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const toUrlEncoded = (payload: Record<string, string>) => const toUrlEncoded = (payload: Record<string, string>) =>
Object.keys(payload) Object.keys(payload)
.map((key) => key + "=" + encodeURIComponent(payload[key])) .map((key) => `${key}=${encodeURIComponent(payload[key])}`)
.join("&"); .join("&");
const body = toUrlEncoded({ const body = toUrlEncoded({
@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
grant_type: "authorization_code", grant_type: "authorization_code",
code, code,
scope: scopes.join(" "), scope: scopes.join(" "),
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`,
client_secret, client_secret,
}); });
@ -52,11 +52,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const responseBody = await response.json(); const responseBody = await response.json();
if (!response.ok) { if (!response.ok) {
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody)); return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
} }
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", { const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: { Authorization: "Bearer " + responseBody.access_token }, headers: { Authorization: `Bearer ${responseBody.access_token}` },
}); });
const graphUser = await whoami.json(); const graphUser = await whoami.json();

View File

@ -343,7 +343,7 @@ export default class Office365CalendarService implements Calendar {
return fetch(`${this.apiGraphUrl}${endpoint}`, { return fetch(`${this.apiGraphUrl}${endpoint}`, {
method: "get", method: "get",
headers: { headers: {
Authorization: "Bearer " + this.accessToken, Authorization: `Bearer ${this.accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
...init, ...init,
@ -479,8 +479,8 @@ export default class Office365CalendarService implements Calendar {
subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => { subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => {
if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc; if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc;
return acc.concat({ return acc.concat({
start: evt.start.dateTime + "Z", start: `${evt.start.dateTime}Z`,
end: evt.end.dateTime + "Z", end: `${evt.end.dateTime}Z`,
}); });
}, []) }, [])
); );

View File

@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
response_type: "code", response_type: "code",
scope: scopes.join(" "), scope: scopes.join(" "),
client_id, client_id,
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/office365video/callback`,
state, state,
}; };
const query = stringify(params); const query = stringify(params);

View File

@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const toUrlEncoded = (payload: Record<string, string>) => const toUrlEncoded = (payload: Record<string, string>) =>
Object.keys(payload) Object.keys(payload)
.map((key) => key + "=" + encodeURIComponent(payload[key])) .map((key) => `${key}=${encodeURIComponent(payload[key])}`)
.join("&"); .join("&");
const body = toUrlEncoded({ const body = toUrlEncoded({
@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
grant_type: "authorization_code", grant_type: "authorization_code",
code, code,
scope: scopes.join(" "), scope: scopes.join(" "),
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/office365video/callback`,
client_secret, client_secret,
}); });
@ -53,11 +53,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const responseBody = await response.json(); const responseBody = await response.json();
if (!response.ok) { if (!response.ok) {
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody)); return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
} }
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", { const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
headers: { Authorization: "Bearer " + responseBody.access_token }, headers: { Authorization: `Bearer ${responseBody.access_token}` },
}); });
const graphUser = await whoami.json(); const graphUser = await whoami.json();

View File

@ -128,7 +128,7 @@ const TeamsVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", { const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(translateEvent(event)), body: JSON.stringify(translateEvent(event)),
@ -152,7 +152,7 @@ const TeamsVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", { const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(translateEvent(event)), body: JSON.stringify(translateEvent(event)),

View File

@ -272,7 +272,7 @@ class Paypal {
webhook_id: options.body.webhook_id, webhook_id: options.body.webhook_id,
}); });
const bodyToString = stringy.slice(0, -1) + `,"webhook_event":${options.body.webhook_event}` + "}"; const bodyToString = `${stringy.slice(0, -1)},"webhook_event":${options.body.webhook_event}}`;
try { try {
const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, { const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, {

View File

@ -19,7 +19,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
const eventTypeURL = eventType.URL + query; const eventTypeURL = eventType.URL + query;
function QRCode({ size, data }: { size: number; data: string }) { function QRCode({ size, data }: { size: number; data: string }) {
const QR_URL = "https://api.qrserver.com/v1/create-qr-code/?size=" + size + "&data=" + data; const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`;
return ( return (
<Tooltip content={eventTypeURL}> <Tooltip content={eventTypeURL}>
<a download href={QR_URL} target="_blank" rel="noreferrer"> <a download href={QR_URL} target="_blank" rel="noreferrer">

View File

@ -108,10 +108,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// Make Header // Make Header
res.write( res.write(
headerFields `${headerFields
.map((field) => `${field.label}${field.deleted ? "(Deleted)" : ""}`) .map((field) => `${field.label}${field.deleted ? "(Deleted)" : ""}`)
.concat(["Submission Time"]) .concat(["Submission Time"])
.join(",") + "\n" .join(",")}\n`
); );
for await (const partialCsv of csvIterator) { for await (const partialCsv of csvIterator) {

View File

@ -44,7 +44,7 @@ export function getQueryBuilderConfig(form: RoutingForm, forReporting = false) {
// preferWidgets: field.type === "textarea" ? ["textarea"] : [], // preferWidgets: field.type === "textarea" ? ["textarea"] : [],
}; };
} else { } else {
throw new Error("Unsupported field type:" + field.type); throw new Error(`Unsupported field type:${field.type}`);
} }
}); });

View File

@ -30,7 +30,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
const fieldsParsed = zodFields.safeParse(form.fields); const fieldsParsed = zodFields.safeParse(form.fields);
if (!fieldsParsed.success) { if (!fieldsParsed.success) {
throw new Error("Error parsing fields" + fieldsParsed.error); throw new Error(`Error parsing fields: ${fieldsParsed.error}`);
} }
const settings = RoutingFormSettings.parse( const settings = RoutingFormSettings.parse(
@ -91,7 +91,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
}, },
}); });
if (!router) { if (!router) {
throw new Error("Form -" + route.id + ", being used as router, not found"); throw new Error(`Form - ${route.id}, being used as router, not found`);
} }
const parsedRouter = await getSerializableForm({ form: router }); const parsedRouter = await getSerializableForm({ form: router });

View File

@ -241,7 +241,7 @@ export default function RoutingForms({
<ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" /> <ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" />
)} )}
<ListLinkItem <ListLinkItem
href={appUrl + "/form-edit/" + form.id} href={`${appUrl}/form-edit/${form.id}`}
heading={form.name} heading={form.name}
disabled={readOnly} disabled={readOnly}
subHeading={description} subHeading={description}

View File

@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const conn = new jsforce.Connection({ const conn = new jsforce.Connection({
clientId: consumer_key, clientId: consumer_key,
clientSecret: consumer_secret, clientSecret: consumer_secret,
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback", redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
}); });
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string); const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);

View File

@ -110,7 +110,7 @@ export default class SalesforceCalendarService implements Calendar {
return new jsforce.Connection({ return new jsforce.Connection({
clientId: consumer_key, clientId: consumer_key,
clientSecret: consumer_secret, clientSecret: consumer_secret,
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback", redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
instanceUrl: credentialKey.instance_url, instanceUrl: credentialKey.instance_url,
accessToken: credentialKey.access_token, accessToken: credentialKey.access_token,
refreshToken: credentialKey.refresh_token, refreshToken: credentialKey.refresh_token,

View File

@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}, },
}); });
const redirect_uri = encodeURI(WEBAPP_URL + "/api/integrations/stripepayment/callback"); const redirect_uri = encodeURI(`${WEBAPP_URL}/api/integrations/stripepayment/callback`);
const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = { const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = {
client_id, client_id,
scope: "read_write", scope: "read_write",

View File

@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (error) { if (error) {
const query = stringify({ error, error_description }); const query = stringify({ error, error_description });
res.redirect("/apps/installed?" + query); res.redirect(`/apps/installed?${query}`);
return; return;
} }

View File

@ -60,7 +60,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
return ( return (
<AppCard <AppCard
returnTo={WEBAPP_URL + pathname + "?tabName=apps"} returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
app={app} app={app}
switchChecked={requirePayment} switchChecked={requirePayment}
switchOnClick={(enabled) => { switchOnClick={(enabled) => {

View File

@ -15,5 +15,5 @@ export function createPaymentLink(opts: {
let link = ""; let link = "";
if (absolute) link = WEBSITE_URL; if (absolute) link = WEBSITE_URL;
const query = stringify({ date, name, email }); const query = stringify({ date, name, email });
return link + `/payment/${paymentUid}?${query}`; return `${link}/payment/${paymentUid}?${query}`;
} }

View File

@ -73,7 +73,7 @@ export async function deleteStripeCustomer(user: UserType): Promise<string | nul
const customerId = await getStripeCustomerId(user); const customerId = await getStripeCustomerId(user);
if (!customerId) { if (!customerId) {
console.warn("No stripe customer found for user:" + user.email); console.warn(`No stripe customer found for user:${user.email}`);
return null; return null;
} }

View File

@ -14,7 +14,7 @@ const SylapsApiAdapter = (): VideoApiAdapter => {
type: "sylaps_video", type: "sylaps_video",
id: meetingID, id: meetingID,
password: "", password: "",
url: "https://sylaps.com/r/" + meetingID, url: `https://sylaps.com/r/${meetingID}`,
}); });
}, },
deleteMeeting: async (): Promise<void> => { deleteMeeting: async (): Promise<void> => {

View File

@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!client_id) return res.status(400).json({ message: "Tandem client_id missing." }); if (!client_id) return res.status(400).json({ message: "Tandem client_id missing." });
if (!base_url) return res.status(400).json({ message: "Tandem base_url missing." }); if (!base_url) return res.status(400).json({ message: "Tandem base_url missing." });
const redirect_uri = encodeURI(WEBAPP_URL + "/api/integrations/tandemvideo/callback"); const redirect_uri = encodeURI(`${WEBAPP_URL}/api/integrations/tandemvideo/callback`);
const params = { const params = {
client_id, client_id,

View File

@ -125,7 +125,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
const result = await fetch(`${base_url}/api/v1/meetings`, { const result = await fetch(`${base_url}/api/v1/meetings`, {
method: "POST", method: "POST",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: _translateEvent(event, "meeting"), body: _translateEvent(event, "meeting"),
@ -141,7 +141,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
await fetch(`${base_url}/api/v1/meetings/${uid}`, { await fetch(`${base_url}/api/v1/meetings/${uid}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
}, },
}).then(handleErrorsRaw); }).then(handleErrorsRaw);
@ -154,7 +154,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
const result = await fetch(`${base_url}/api/v1/meetings/${bookingRef.meetingId}`, { const result = await fetch(`${base_url}/api/v1/meetings/${bookingRef.meetingId}`, {
method: "PUT", method: "PUT",
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: _translateEvent(event, "updates"), body: _translateEvent(event, "updates"),

View File

@ -2,6 +2,7 @@
items: items:
- 1.jpg - 1.jpg
--- ---
Vital App is an app that can can help you combine your health peripherals with your calendar. Vital App is an app that can can help you combine your health peripherals with your calendar.
#### Supported Actions: #### Supported Actions:

View File

@ -45,7 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const token = await vitalClient.Link.create( const token = await vitalClient.Link.create(
userVital?.user_id, userVital?.user_id,
undefined, undefined,
WEBAPP_URL + "/api/integrations/vital/callback" `${WEBAPP_URL}/api/integrations/vital/callback`
); );
return res.status(200).json({ return res.status(200).json({
token: token.link_token, token: token.link_token,

View File

@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
/** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/ /** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`); const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`);
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64"); const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
const result = await fetch( const result = await fetch(
`https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id=${client_id}&client_secret=${client_secret}&code=${code}&redirect_uri=${redirectUri}`, `https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id=${client_id}&client_secret=${client_secret}&code=${code}&redirect_uri=${redirectUri}`,
{ {

View File

@ -154,7 +154,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
method: "GET", method: "GET",
...options, ...options,
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: `Bearer ${accessToken}`,
...options?.headers, ...options?.headers,
}, },
}); });
@ -208,7 +208,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
url: result.webLink, url: result.webLink,
}; };
} }
throw new Error("Failed to create meeting. Response is " + JSON.stringify(result)); throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
throw new Error("Unexpected error"); throw new Error("Unexpected error");
@ -258,7 +258,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
url: result.webLink, url: result.webLink,
}; };
} }
throw new Error("Failed to create meeting. Response is " + JSON.stringify(result)); throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
throw new Error("Unexpected error"); throw new Error("Unexpected error");

View File

@ -27,7 +27,7 @@ const wipeMyCalAction = async (props: IWipeMyCalAction) => {
}; };
try { try {
const endpoint = "/api/integrations/wipemycalother/wipe"; const endpoint = "/api/integrations/wipemycalother/wipe";
return fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}` + endpoint, { return fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}${endpoint}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",

View File

@ -26,7 +26,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
? authorizedAccount.name ? authorizedAccount.name
: null; : null;
const requested = teamInfo ? "team: " + teamInfo : "user: " + userInfo; const requested = teamInfo ? `team: ${teamInfo}` : `user: ${userInfo}`;
return res.status(404).json({ return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``, message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
}); });

View File

@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : ""; const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." }); if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
const redirectUri = WEBAPP_URL + `/api/integrations/zoho-bigin/callback`; const redirectUri = `${WEBAPP_URL}/api/integrations/zoho-bigin/callback`;
const authUrl = axios.getUri({ const authUrl = axios.getUri({
url: "https://accounts.zoho.com/oauth/v2/auth", url: "https://accounts.zoho.com/oauth/v2/auth",

View File

@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!clientSecret) return res.status(400).json({ message: "Zoho Bigin client_secret missing." }); if (!clientSecret) return res.status(400).json({ message: "Zoho Bigin client_secret missing." });
const accountsUrl = `${accountsServer}/oauth/v2/token`; const accountsUrl = `${accountsServer}/oauth/v2/token`;
const redirectUri = WEBAPP_URL + `/api/integrations/${appConfig.slug}/callback`; const redirectUri = `${WEBAPP_URL}/api/integrations/${appConfig.slug}/callback`;
const formData = { const formData = {
client_id: clientId, client_id: clientId,

View File

@ -148,8 +148,9 @@ export default class BiginCalendarService implements Calendar {
*/ */
private async contactSearch(event: CalendarEvent) { private async contactSearch(event: CalendarEvent) {
const token = await this.auth.getToken(); const token = await this.auth.getToken();
const searchCriteria = const searchCriteria = `(${event.attendees
"(" + event.attendees.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`).join("or") + ")"; .map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`)
.join("or")})`;
return await axios({ return await axios({
method: "get", method: "get",
@ -298,21 +299,9 @@ const toISO8601String = (date: Date) => {
return (num < 10 ? "0" : "") + num; return (num < 10 ? "0" : "") + num;
}; };
return ( return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
date.getFullYear() + date.getHours()
"-" + )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${dif}${pad(Math.floor(Math.abs(tzo) / 60))}:${pad(
pad(date.getMonth() + 1) + Math.abs(tzo) % 60
"-" + )}`;
pad(date.getDate()) +
"T" +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds()) +
dif +
pad(Math.floor(Math.abs(tzo) / 60)) +
":" +
pad(Math.abs(tzo) % 60)
);
}; };

View File

@ -4,13 +4,13 @@
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one. 1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. Create a "Server-based Applications", set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs. 2. Create a "Server-based Applications", set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs.
4. Fill in any information you want in the "Client Details" tab 3. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab. 4. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`<Cal.com>/settings/admin/apps`). 5. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`<Cal.com>/settings/admin/apps`).
7. Back in Zoho API Console, 6. Back in Zoho API Console,
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers. 7. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
9. Click the "Save"/ "UPDATE" button at the bottom footer. 8. Click the "Save"/ "UPDATE" button at the bottom footer.
10. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings at `/settings/my-account/calendars`. 9. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings at `/settings/my-account/calendars`.
11. You can access your Zoho calendar at [https://calendar.zoho.com/](https://calendar.zoho.com/) 10. You can access your Zoho calendar at [https://calendar.zoho.com/](https://calendar.zoho.com/)
NOTE: If you use multiple calendars with Cal, make sure you enable the toggle to prevent double-bookings across calendar. This is in `/settings/my-account/calendars`. NOTE: If you use multiple calendars with Cal, make sure you enable the toggle to prevent double-bookings across calendar. This is in `/settings/my-account/calendars`.

View File

@ -20,7 +20,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const params = { const params = {
client_id, client_id,
response_type: "code", response_type: "code",
redirect_uri: WEBAPP_URL + "/api/integrations/zohocalendar/callback", redirect_uri: `${WEBAPP_URL}/api/integrations/zohocalendar/callback`,
scope: [ scope: [
"ZohoCalendar.calendar.ALL", "ZohoCalendar.calendar.ALL",
"ZohoCalendar.event.ALL", "ZohoCalendar.event.ALL",

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