This commit is contained in:
Peer Richelsen
2023-05-05 14:58:47 +01:00
204 changed files with 1539 additions and 826 deletions
+2 -1
View File
@@ -16,11 +16,12 @@
# - WEB3
# - SALESFORCE
# - ZOHOCRM
# - ZOHO_BIGIN
# - APP STORE **********************************************************************************************
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
# - DAILY.CO VIDEO
# Enables Cal Video. to get your key
# Enables Cal Video. to get your key
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
+2 -1
View File
@@ -65,8 +65,9 @@ tsconfig.tsbuildinfo
# turbo
.turbo
# Prisma-Zod
# Prisma generated files
packages/prisma/zod/*.ts
packages/prisma/enums
# Builds
dist
+1
View File
@@ -14,4 +14,5 @@ public
.DS_Store
.eslintignore
packages/prisma/zod
packages/prisma/enums
apps/web/public/embed
+45 -43
View File
@@ -24,7 +24,7 @@
<p align="center">
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
<a href="https://status.cal.com"><img height="20px" src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
<a href="https://news.ycombinator.com/item?id=34507672"><img src="https://img.shields.io/badge/Hacker%20News-%231-%23FF6600" alt="Hacker News"></a>
@@ -38,13 +38,13 @@
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
<a href="https://contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
<a href="https://console.algora.io/org/cal/bounties?status=open"><img src="https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcal%2Fbounties%3Fstatus%3Dopen" /></a>
</p>
<!-- ABOUT THE PROJECT -->
## About The Project
## About the Project
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/176390354-f1bc7069-0341-437a-9bb8-eb41092b4016.gif">
@@ -53,13 +53,13 @@
The open source Calendly alternative. You are in charge
of your own data, workflow and appearance.
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customisations.
Calendly and other scheduling tools are awesome. It made our lives massively easier. We're using it for business meetings, seminars, yoga classes and even calls with our families. However, most tools are very limited in terms of control and customizations.
That's where Cal.com comes in. Self-hosted or hosted by us. White-label by design. API-driven and ready to be deployed on your own domain. Full control of your events and data.
## Recognition
#### Hacker News
#### [Hacker News](https://news.ycombinator.com/from?site=cal.com)
<a href="https://news.ycombinator.com/item?id=34507672">
<img
@@ -77,22 +77,22 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
/>
</a>
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
#### [Product Hunt](https://producthunt.com/products/cal-com?utm_source=badge-top-post-badge&utm_medium=badge)
<a href="https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=291910&theme=light&period=monthly" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/posts/calendso?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-calendso" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=291910&theme=light" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://producthunt.com/stories/how-this-open-source-calendly-alternative-rocketed-to-product-of-the-day" target="_blank"><img src="https://cal.com/maker-grant.svg" alt="Cal.com - The open source Calendly alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
### Built With
- [Next.js](https://nextjs.org/?ref=cal.com)
- [tRPC](https://trpc.io/?ref=cal.com)
- [React](https://reactjs.org/?ref=cal.com)
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
- [Prisma](https://prisma.io/?ref=cal.com)
- [React.js](https://reactjs.org/?ref=cal.com)
- [Tailwind CSS](https://tailwindcss.com/?ref=cal.com)
- [Prisma.io](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily)
## Stay Up-to-Date
Cal officially launched as v.1.0 on 15th of September, however a lot of new features are coming. Watch **releases** of this repository to be notified for future updates:
Cal.com officially launched as v.1.0 on 15th of September, however a lot of new features are coming. Watch **releases** of this repository to be notified for future updates:
![cal-star-github](https://user-images.githubusercontent.com/8019099/154853944-a9e3c999-3da3-4048-b149-b4f73893c6fb.gif)
@@ -104,7 +104,7 @@ To get a local copy up and running, please follow these simple steps.
### Prerequisites
Here is what you need to be able to run Cal.
Here is what you need to be able to run Cal.com.
- Node.js (Version: >=15.x <17)
- PostgreSQL
@@ -134,10 +134,10 @@ Here is what you need to be able to run Cal.
yarn
```
1. Set up your .env file
1. Set up your `.env` file
- 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 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` 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.
#### Quick start with `yarn dx`
@@ -150,7 +150,7 @@ yarn dx
#### Development tip
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **trpc**.
> Add `NEXT_PUBLIC_DEBUG=1` anywhere in your `.env` to get logging information for all the queries and mutations driven by **tRPC**.
```sh
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
@@ -158,16 +158,16 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
#### Manual setup
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, and `<db-port>` with their applicable values
```
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
```
<details>
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku</summary>
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick DB using Heroku.</summary>
1. Create a free account with [Heroku](https://www.heroku.com/).
1. Create a free account with [Heroku](https://heroku.com).
2. Create a new app.
<img width="306" alt="Create an App" src="https://user-images.githubusercontent.com/16905768/115322780-b3d58c00-a17e-11eb-8a52-b758fb0ea942.png">
@@ -184,14 +184,14 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`.
![image](https://user-images.githubusercontent.com/16905768/115323367-e92ea980-a17f-11eb-9ff4-dec95f2ec349.png)
7. In `Settings`, copy your URI to your Cal.com .env file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
7. In `Settings`, copy your URI to your Cal.com `.env` file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
![image](https://user-images.githubusercontent.com/16905768/115323556-4591c900-a180-11eb-9808-2f55d2aa3995.png)
![image](https://user-images.githubusercontent.com/16905768/115323697-7a9e1b80-a180-11eb-9f08-a742b1037f90.png)
8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com/).
8. To view your DB, once you add new data in Prisma, you can use [Heroku Data Explorer](https://heroku-data-explorer.herokuapp.com).
</details>
1. Set a 32 character random string in your .env file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
```sh
@@ -206,7 +206,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
#### Setting up your first user
1. Open [Prisma Studio](https://www.prisma.io/studio) to look at or modify the database content:
1. Open [Prisma Studio](https://prisma.io/studio) to look at or modify the database content:
```sh
yarn db-studio
@@ -251,7 +251,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
yarn workspace @calcom/prisma db-migrate
```
(this can clear your development database in some cases)
(This can clear your development database in some cases)
In a production environment, run:
@@ -285,7 +285,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
### Docker
The Docker configuration for Cal is an effort powered by people within the community.
The Docker configuration for Cal.com is an effort powered by people within the community.
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
@@ -298,14 +298,14 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
### Heroku
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
<img width="185px" height="auto" src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy">
<img width="185px" height="auto" src="https://herokucdn.com/deploy/button.svg" alt="Deploy">
</a>
### Railway
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/cal)
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
### Vercel
@@ -386,7 +386,7 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
8. Select Web Application as the Application Type.
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the `.env` file as the value for `GOOGLE_API_CREDENTIALS` key.
#### _Adding google calendar to Cal.com App Store_
@@ -423,7 +423,7 @@ following
5. Choose "User-managed app" as the app type.
6. De-select the option to publish the app on the Zoom App Marketplace.
7. Click "Create".
8. Now copy the Client ID and Client Secret to your .env file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
@@ -435,8 +435,8 @@ following
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
3. Copy your API key.
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
5. If you have the [Daily Scale Plan](https://www.daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
4. Now paste the API key to your `.env` file into the `DAILY_API_KEY` field in your `.env` file.
5. If you have the [Daily Scale Plan](https://daily.co/pricing) set the `DAILY_SCALE_PLAN` variable to `true` in order to use features like video recording.
### Obtaining HubSpot Client ID and Secret
@@ -445,7 +445,7 @@ following
3. Click "Create app" button top right.
4. Fill in any information you want in the "App info" tab
5. Go to tab "Auth"
6. Now copy the Client ID and Client Secret to your .env file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
6. Now copy the Client ID and Client Secret to your `.env` file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs.
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
9. Click the "Save" button at the bottom footer.
@@ -458,31 +458,33 @@ following
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
4. Fill in any information you want in the "Client Details" tab
5. Go to tab "Client Secret" tab.
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
6. Now copy the Client ID and Client Secret to your `.env` file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
8. 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.
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
### Obtaining Zoho Bigin Client ID and Secret
[Follow these steps](./packages/app-store/zoho-bigin/)
## Workflows
### Setting up SendGrid for Email reminders
1. Create a SendGrid account (https://signup.sendgrid.com/)
2. Go to Settings -> API keys and create an API key
3. Copy API key to your .env file into the SENDGRID_API_KEY field
3. Copy API key to your `.env` file into the `SENDGRID_API_KEY` field
4. Go to Settings -> Sender Authentication and verify a single sender
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
6. Add your custom sender name to the .env file into the NEXT_PUBLIC_SENDGRID_SENDER_NAME field (fallback is Cal.com)
5. Copy the verified E-Mail to your `.env` file into the `SENDGRID_EMAIL` field
6. Add your custom sender name to the `.env` file into the `NEXT_PUBLIC_SENDGRID_SENDER_NAME` field (fallback is Cal.com)
### Setting up Twilio for SMS reminders
1. Create a Twilio account (https://www.twilio.com/try-twilio)
1. Create a Twilio account (https://twilio.com/try-twilio)
2. Click Get a Twilio phone number
3. Copy Account SID to your .env file into the TWILIO_SID field
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
3. Copy Account SID to your `.env` file into the `TWILIO_SID` field
4. Copy Auth Token to your `.env` file into the `TWILIO_TOKEN` field
5. Copy your Twilio phone number to your `.env` file into the `TWILIO_PHONE_NUMBER` field
6. Add your own sender id to the `.env` file into the `NEXT_PUBLIC_SENDER_ID` field (fallback is Cal.com)
7. Create a messaging service (Develop -> Messaging -> Services)
8. Choose any name for the messaging service
9. Click 'Add Senders'
@@ -490,9 +492,9 @@ following
11. Add the listed phone number
12. Leave all other fields as they are
13. Complete setup and click View my new Messaging Service
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
14. Copy Messaging Service SID to your `.env` file into the `TWILIO_MESSAGING_SID` field
15. Create a verify service
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
16. Copy Verify Service SID to your `.env` file into the `TWILIO_VERIFY_SID` field
<!-- LICENSE -->
+264 -273
View File
@@ -1,5 +1,6 @@
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import type { TFunction } from "next-i18next";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { MutableRefObject, RefObject } from "react";
@@ -21,11 +22,10 @@ import {
TextArea,
TextField,
ColorPicker,
Select,
} from "@calcom/ui";
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
import Select from "@components/ui/form/Select";
type EmbedType = "inline" | "floating-popup" | "element-click";
type EmbedFramework = "react" | "HTML";
@@ -305,190 +305,189 @@ const getEmbedTypeSpecificString = ({
return "";
};
const embeds: {
illustration: React.ReactElement;
title: string;
subtitle: string;
type: EmbedType;
}[] = [
{
title: "Inline Embed",
subtitle: "Loads your event type directly inline with your other website content.",
type: "inline",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
fill="#3E3E3E"
/>
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
</svg>
),
},
{
title: "Floating pop-up button",
subtitle: "Adds a floating button on your site that launches Cal in a dialog.",
type: "floating-popup",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
</svg>
),
},
{
title: "Pop up via element click",
subtitle: "Open your Cal dialog when someone clicks an element.",
type: "element-click",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
<g filter="url(#filter0_i_3223_14162)">
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
</g>
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
const embeds = (t: TFunction) =>
(() => {
return [
{
title: t("inline_embed"),
subtitle: t("load_inline_content"),
type: "inline",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<path
d="M61 124.5H67V122.5H61V124.5ZM68 125.5V131.5H70V125.5H68ZM67 132.5H61V134.5H67V132.5ZM60 131.5V125.5H58V131.5H60ZM61 132.5C60.4477 132.5 60 132.052 60 131.5H58C58 133.157 59.3431 134.5 61 134.5V132.5ZM68 131.5C68 132.052 67.5523 132.5 67 132.5V134.5C68.6569 134.5 70 133.157 70 131.5H68ZM67 124.5C67.5523 124.5 68 124.948 68 125.5H70C70 123.843 68.6569 122.5 67 122.5V124.5ZM61 122.5C59.3431 122.5 58 123.843 58 125.5H60C60 124.948 60.4477 124.5 61 124.5V122.5Z"
fill="#3E3E3E"
/>
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="109" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="121" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="121" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="61" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
</svg>
),
},
{
title: t("floating_pop_up_button"),
subtitle: t("floating_button_trigger_modal"),
type: "floating-popup",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
</svg>
),
},
{
title: t("pop_up_element_click"),
subtitle: t("open_dialog_with_element_click"),
type: "element-click",
illustration: (
<svg
width="100%"
height="100%"
className="rounded-md"
viewBox="0 0 308 265"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
fill="white"
/>
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
<g filter="url(#filter0_i_3223_14162)">
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
</g>
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
</svg>
),
},
];
})();
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
</svg>
),
},
];
const tabs = [
{
name: "HTML",
@@ -534,11 +533,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
<!-- Cal ${embedType} embed code ends -->`
}
/>
<p className="text-subtle hidden text-sm">
{t(
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
)}
</p>
<p className="text-subtle hidden text-sm">{t("need_help_embedding")}</p>
</>
);
}),
@@ -622,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
return (
<components.Control {...props}>
<Sun className="text-subtle ml-2 h-4 w-4" />
<Sun className="text-subtle mr-2 h-4 w-4" />
{children}
</components.Control>
);
@@ -642,7 +637,7 @@ const ChooseEmbedTypesDialogContent = () => {
</div>
</div>
<div className="items-start space-y-2 md:flex md:space-y-0">
{embeds.map((embed, index) => (
{embeds(t).map((embed, index) => (
<button
className="hover:bg-subtle bg-muted w-full rounded-md border border-transparent p-6 text-left hover:rounded-md ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0 lg:w-1/3"
key={index}
@@ -692,7 +687,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
const embed = embeds.find((embed) => embed.type === embedType);
const embed = embeds(t).find((embed) => embed.type === embedType);
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
@@ -806,11 +801,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
const FloatingPopupPositionOptions = [
{
value: "bottom-right",
label: "Bottom Right",
label: "Bottom right",
},
{
value: "bottom-left",
label: "Bottom Left",
label: "Bottom left",
},
];
@@ -834,7 +829,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</button>
{embed.title}
</h3>
<h4 className="text-emphasis mb-6 text-sm font-normal">{embed.subtitle}</h4>
<h4 className="text-subtle mb-6 text-sm font-normal">{embed.subtitle}</h4>
<div className="flex flex-col">
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
@@ -844,7 +839,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
{/*TODO: Add Auto/Fixed toggle from Figma */}
<div className="text-default mb-[9px] text-sm">Window sizing</div>
<div className="justify-left flex items-center !font-normal">
<div className="justify-left mb-6 flex items-center !font-normal ">
<div className="mr-[9px]">
<TextField
labelProps={{ className: "hidden" }}
@@ -892,10 +887,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
"items-center justify-between",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2 text-sm">Button Text</div>
<div className="mb-2 text-sm">Button text</div>
{/* Default Values should come from preview iframe */}
<TextField
labelProps={{ className: "hidden" }}
@@ -910,7 +905,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
};
});
}}
defaultValue="Book my Cal"
defaultValue={t("book_my_cal")}
required
/>
</div>
@@ -935,14 +930,14 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
});
}}
/>
<div className="text-default text-sm">Display Calendar Icon Button</div>
<div className="text-default my-2 text-sm">Display calendar icon</div>
</div>
<div
className={classNames(
"mt-4 items-center justify-between",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div className="mb-2">Position of Button</div>
<div className="mb-2">Position of button</div>
<Select
onChange={(position) => {
setPreviewState((previewState) => {
@@ -959,54 +954,50 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
options={FloatingPopupPositionOptions}
/>
</div>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Button Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
},
};
});
}}
/>
<div className="mt-3 flex flex-col xl:flex-row xl:justify-between">
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div className="whitespace-nowrap">Button color</div>
<div className="mt-2 w-40 xl:mt-0 xl:w-full">
<ColorPicker
className="w-[130px]"
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonColor: color,
},
};
});
}}
/>
</div>
</div>
</div>
<div
className={classNames(
"mt-4",
embedType === "floating-popup" ? "text-emphasis" : "hidden"
)}>
<div>Text Color</div>
<div className="w-full">
<ColorPicker
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
},
};
});
}}
/>
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
<div className="whitespace-nowrap">Text color</div>
<div className="mt-2 mb-6 w-40 xl:mt-0 xl:w-full">
<ColorPicker
className="w-[130px]"
popoverAlign="start"
container={dialogContentRef?.current ?? undefined}
defaultValue="#000000"
onChange={(color) => {
setPreviewState((previewState) => {
return {
...previewState,
floatingPopup: {
...previewState.floatingPopup,
buttonTextColor: color,
},
};
});
}}
/>
</div>
</div>
</div>
</CollapsibleContent>
@@ -1017,7 +1008,30 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
open={isBookingCustomizationOpen}
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
<CollapsibleContent>
<div className="mt-6 text-sm">
<div className="text-sm">
<Label className="mb-6">
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
@@ -1056,29 +1070,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</div>
</Label>
))}
<Label>
<div className="mb-2">Theme</div>
<Select
className="w-full"
defaultValue={ThemeOptions[0]}
components={{
Control: ThemeSelectControl,
IndicatorSeparator: () => null,
}}
onChange={(option) => {
if (!option) {
return;
}
setPreviewState((previewState) => {
return {
...previewState,
theme: option.value,
};
});
}}
options={ThemeOptions}
/>
</Label>
</div>
</CollapsibleContent>
</Collapsible>
+1 -3
View File
@@ -1,5 +1,4 @@
import Link from "next/link";
import { useRouter } from "next/router";
import type { IframeHTMLAttributes } from "react";
import React, { useState } from "react";
@@ -43,7 +42,6 @@ const Component = ({
}: Parameters<typeof App>[0]) => {
const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
const router = useRouter();
const mutation = useAddAppMutation(null, {
onSuccess: (data) => {
@@ -186,7 +184,7 @@ const Component = ({
label={t("disconnect")}
credentialId={existingCredentials[0]}
onSuccess={() => {
router.replace("/apps/installed");
appCredentials.refetch();
}}
/>
) : (
@@ -1,4 +1,3 @@
import { UserPermissionRole } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import type { ComponentProps } from "react";
@@ -6,6 +5,7 @@ import React, { useEffect } from "react";
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import type Shell from "@calcom/features/shell/Shell";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { ErrorBoundary } from "@calcom/ui";
export default function AdminLayout({
@@ -1,10 +1,10 @@
import { SchedulingType } from "@prisma/client";
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Badge } from "@calcom/ui";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
@@ -1,4 +1,3 @@
import { BookingStatus } from "@prisma/client";
import { useRouter } from "next/router";
import { useState } from "react";
@@ -13,6 +12,7 @@ import { formatTime } from "@calcom/lib/date-fns";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { BookingStatus } from "@calcom/prisma/enums";
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { ActionType } from "@calcom/ui";
@@ -554,7 +554,7 @@ const BookingPage = ({
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="dark:text-inverted text-default items-start text-sm font-medium">
<div className="text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
@@ -1,4 +1,3 @@
import { SchedulingType } from "@prisma/client";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
@@ -10,6 +9,7 @@ import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hook
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
@@ -13,7 +13,7 @@ import type { DurationType } from "@calcom/lib/convertToNewDurationType";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { PeriodType } from "@calcom/prisma/client";
import type { PeriodType } from "@calcom/prisma/enums";
import type { IntervalLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
import { Plus, Trash } from "@calcom/ui/components/icon";
@@ -276,7 +276,7 @@ export const EventSetupTab = (
return (
<li
key={`${location.type}${index}`}
className="border-default text-default mb-2 rounded-md border py-1.5 px-2 hover:cursor-pointer">
className="border-default text-default mb-2 h-9 rounded-md border py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center">
<img
@@ -466,6 +466,7 @@ export const EventSetupTab = (
defaultValue={eventType.length ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
min={1}
/>
)}
{!lengthLockedProps.disabled && (
@@ -1,4 +1,3 @@
import { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
@@ -10,6 +9,7 @@ import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTea
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Label, Select } from "@calcom/ui";
interface IUserToValue {
@@ -1,4 +1,3 @@
import { SchedulingType } from "@prisma/client";
import { Webhook as TbWebhook } from "lucide-react";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
@@ -13,6 +12,7 @@ import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Button,
@@ -238,7 +238,7 @@ function EventTypeSingleLayout({
<div className="flex items-center justify-end">
{!eventType.metadata.managedEventConfig && (
<>
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 lg:flex">
<Skeleton
as={Label}
htmlFor="hiddenSwitch"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.8.10",
"version": "2.8.12",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -1
View File
@@ -1,4 +1,3 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
@@ -6,6 +5,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession({ req, res });
+1 -1
View File
@@ -1,4 +1,3 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest } from "next";
import z from "zod";
@@ -8,6 +7,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
const querySchema = z.object({
username: z
+15 -4
View File
@@ -1,10 +1,10 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
@@ -36,14 +36,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return;
}
// There is actually an existingUser if username matches
// OR if email matches and both username and password are set
// There is an existingUser if the username matches
// OR if the email matches AND either the email is verified
// or both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [{ email: userEmail }],
AND: [
{ email: userEmail },
{
OR: [
{ emailVerified: { not: null } },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
},
],
},
@@ -1,4 +1,3 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
@@ -8,6 +7,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
+1 -1
View File
@@ -1,4 +1,3 @@
import { BookingStatus, ReminderType } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
@@ -7,6 +6,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+1 -1
View File
@@ -1,10 +1,10 @@
import { UserPermissionRole } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
@@ -1,4 +1,3 @@
import { AppCategories } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -7,6 +6,7 @@ import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums";
import { AppCard, SkeletonText } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper";
+1 -1
View File
@@ -6,7 +6,7 @@ import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import type { AppCategories } from "@calcom/prisma/enums";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { HorizontalTabItemProps } from "@calcom/ui";
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
+1 -1
View File
@@ -1,4 +1,3 @@
import { UserPermissionRole } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
@@ -8,6 +7,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Meta, WizardForm } from "@calcom/ui";
+1 -1
View File
@@ -188,7 +188,7 @@ export default function Availability() {
}
CTA={
<div className="flex items-center justify-end">
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 sm:flex">
<Skeleton as={Label} htmlFor="hiddenSwitch" className="mt-2 cursor-pointer self-center pr-2 ">
{t("set_to_default")}
</Skeleton>
+5 -3
View File
@@ -1,4 +1,3 @@
import { BookingStatus } from "@prisma/client";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import classNames from "classnames";
import { createEvent } from "ics";
@@ -28,6 +27,7 @@ import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booki
import {
SystemField,
getBookingFieldsWithSystemFields,
SMS_REMINDER_NUMBER_FIELD,
} from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import { APP_NAME } from "@calcom/lib/constants";
@@ -46,6 +46,7 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
import { localStorage } from "@calcom/lib/webstorage";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme } from "@calcom/ui";
@@ -308,7 +309,7 @@ export default function Success(props: SuccessProps) {
<div className="mt-2 ml-4 -mb-4">
<Link
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
className="hover:bg-subtle text-subtle dark:hover:text-inverted hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
className="hover:bg-subtle text-subtle hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
<ChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
</Link>
</div>
@@ -487,7 +488,8 @@ export default function Success(props: SuccessProps) {
// We show rescheduleReason at the top
if (!field) return null;
const isSystemField = SystemField.safeParse(field.name);
if (isSystemField.success) return null;
// SMS_REMINDER_NUMBER_FIELD is a system field but doesn't have a dedicated place in the UI. So, it would be shown through the following responses list
if (isSystemField.success && field.name !== SMS_REMINDER_NUMBER_FIELD) return null;
const label = field.label || t(field.defaultLabel || "");
+8 -1
View File
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType, SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useEffect, useState } from "react";
@@ -19,6 +18,7 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { Prisma } from "@calcom/prisma/client";
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
@@ -136,6 +136,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
formMethods.setValue(
"children",
formMethods.getValues().children.map((child) => ({
...child,
created: true,
}))
);
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
+1 -1
View File
@@ -1,6 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -18,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import {
@@ -146,6 +146,7 @@ const AppearanceView = () => {
<p className="text-default mb-2 block text-sm font-medium">{t("light_brand_color")}</p>
<ColorPicker
defaultValue={user.brandColor}
resetDefaultValue="#292929"
onChange={(value) => {
if (!checkWCAGContrastColor("#ffffff", value)) {
setLightModeError(true);
@@ -167,6 +168,7 @@ const AppearanceView = () => {
<p className="text-default mb-2 block text-sm font-medium">{t("dark_brand_color")}</p>
<ColorPicker
defaultValue={user.darkBrandColor}
resetDefaultValue="#fafafa"
onChange={(value) => {
if (!checkWCAGContrastColor("#101010", value)) {
setDarkModeError(true);
@@ -1,5 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { IdentityProvider } from "@prisma/client";
import { signOut } from "next-auth/react";
import type { BaseSyntheticEvent } from "react";
import { useRef, useState } from "react";
@@ -13,6 +12,7 @@ import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import turndown from "@calcom/lib/turndownService";
import { IdentityProvider } from "@calcom/prisma/enums";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
@@ -1,10 +1,10 @@
import { IdentityProvider } from "@prisma/client";
import { signOut, useSession } from "next-auth/react";
import { useForm } from "react-hook-form";
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { IdentityProvider } from "@calcom/prisma/enums";
import { userMetadata } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { Alert, Button, Form, Meta, PasswordField, Select, SettingsToggle, showToast } from "@calcom/ui";
@@ -199,6 +199,7 @@ const PasswordView = () => {
color="primary"
className="mt-8"
type="submit"
onClick={() => formMethods.clearErrors("apiError")}
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
{t("update")}
</Button>
+1
View File
@@ -12,6 +12,7 @@ test.describe("App Store - Authed", () => {
await pro.login();
await page.goto("/apps/categories/calendar");
await page.click('[data-testid="app-store-app-card-apple-calendar"]');
await page.waitForURL("/apps/apple-calendar");
await page.click('[data-testid="install-app-button"]');
await expect(page.locator(`text=Connect to Apple Server`)).toBeVisible();
});
@@ -19,7 +19,10 @@ test("Can delete user account", async ({ page, users }) => {
const $passwordField = page.locator("[data-testid=password]");
await $passwordField.fill(user.username);
await Promise.all([page.waitForNavigation({ url: "/auth/logout" }), page.click("text=Delete my account")]);
await Promise.all([
page.waitForURL((url) => url.pathname === "/auth/logout"),
page.click("text=Delete my account"),
]);
await expect(page.locator(`[id="modal-title"]`)).toHaveText("You've been logged out");
});
@@ -17,9 +17,7 @@ test("Can reset forgotten password", async ({ page, users }) => {
// Press Enter
await Promise.all([
page.waitForNavigation({
url: (u) => u.pathname.startsWith("/auth/forgot-password/"),
}),
page.waitForURL((u) => u.pathname.startsWith("/auth/forgot-password/")),
page.press('input[name="email"]', "Enter"),
]);
@@ -38,7 +36,7 @@ test("Can reset forgotten password", async ({ page, users }) => {
await expect(page.locator(`text=Password updated`)).toBeVisible();
// Click button:has-text("Login")
await Promise.all([
page.waitForNavigation({ url: (u) => u.pathname.startsWith("/auth/login") }),
page.waitForURL((u) => u.pathname.startsWith("/auth/login")),
page.click('a:has-text("Login")'),
]);
+15 -18
View File
@@ -31,10 +31,8 @@ testBothBookers.describe("free user", (bookerVariant) => {
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant !== "new-booker") {
// Navigate to book page
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/book");
},
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
@@ -84,23 +82,24 @@ testBothBookers.describe("pro user", () => {
await page.waitForSelector('[data-testid="bookings"]');
await page.locator('[data-testid="edit_booking"]').nth(0).click();
await page.locator('[data-testid="reschedule"]').click();
await page.waitForNavigation({
url: (url) => {
const bookingId = url.searchParams.get("rescheduleUid");
return !!bookingId;
},
await page.waitForURL((url) => {
const bookingId = url.searchParams.get("rescheduleUid");
return !!bookingId;
});
await selectSecondAvailableTimeSlotNextMonth(page);
// --- fill form
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname.startsWith("/booking");
},
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
});
test("Can cancel the recently created booking and rebook the same timeslot", async ({ page, users }) => {
test("Can cancel the recently created booking and rebook the same timeslot", async ({
page,
users,
}, testInfo) => {
// Because it tests the entire booking flow + the cancellation + rebooking
test.setTimeout(testInfo.timeout * 3);
await bookFirstEvent(page);
const [pro] = users.get();
@@ -108,10 +107,8 @@ testBothBookers.describe("pro user", () => {
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/booking");
},
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking/");
});
await page.locator('[data-testid="cancel"]').click();
+5 -6
View File
@@ -1,9 +1,9 @@
import { expect } from "@playwright/test";
import type { Prisma } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import { v4 as uuidv4 } from "uuid";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
@@ -71,10 +71,8 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
// to change the test as little as possible.
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant === "old-booker") {
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/book");
},
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
@@ -192,7 +190,7 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({ url: /.*booking/ });
await page.waitForURL(/.*booking/);
await page.goto(`/reschedule/${references[1].referenceUid}`);
@@ -200,6 +198,7 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
// Using waitForUrl here fails the assertion `expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);` probably because waitForUrl is considered complete before waitForNavigation and till that time the booking is not cancelled
await page.waitForNavigation({ url: /.*booking/ });
// Should expect old booking to be cancelled
@@ -31,19 +31,15 @@ test.skip("dynamic booking", async ({ page, users }) => {
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="edit_booking"]').nth(0).click();
await page.locator('[data-testid="reschedule"]').click();
await page.waitForNavigation({
url: (url) => {
const bookingId = url.searchParams.get("rescheduleUid");
return !!bookingId;
},
await page.waitForURL((url) => {
const bookingId = url.searchParams.get("rescheduleUid");
return !!bookingId;
});
await selectSecondAvailableTimeSlotNextMonth(page);
// --- fill form
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname.startsWith("/booking");
},
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
@@ -51,10 +47,8 @@ test.skip("dynamic booking", async ({ page, users }) => {
await test.step("Can cancel the recently created booking", async () => {
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({
url: (url) => {
return url.pathname.startsWith("/booking");
},
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await page.locator('[data-testid="cancel"]').click();
+15 -21
View File
@@ -36,14 +36,12 @@ async function expectToBeNavigatingToEmbedTypesDialog(
if (!embedUrl) {
throw new Error("Couldn't find embedUrl");
}
await page.waitForNavigation({
url: (url) => {
return (
url.pathname === basePage &&
url.searchParams.get("dialog") === "embed" &&
url.searchParams.get("embedUrl") === embedUrl
);
},
await page.waitForURL((url) => {
return (
url.pathname === basePage &&
url.searchParams.get("dialog") === "embed" &&
url.searchParams.get("embedUrl") === embedUrl
);
});
}
@@ -54,16 +52,14 @@ async function expectToBeNavigatingToEmbedCodeAndPreviewDialog(
if (!embedUrl) {
throw new Error("Couldn't find embedUrl");
}
await page.waitForNavigation({
url: (url) => {
return (
url.pathname === basePage &&
url.searchParams.get("dialog") === "embed" &&
url.searchParams.get("embedUrl") === embedUrl &&
url.searchParams.get("embedType") === embedType &&
url.searchParams.get("embedTabName") === "embed-code"
);
},
await page.waitForURL((url) => {
return (
url.pathname === basePage &&
url.searchParams.get("dialog") === "embed" &&
url.searchParams.get("embedUrl") === embedUrl &&
url.searchParams.get("embedType") === embedType &&
url.searchParams.get("embedTabName") === "embed-code"
);
});
}
@@ -183,9 +179,7 @@ test.describe("Embed Code Generator Tests", () => {
await page.goto(`/event-types`);
await Promise.all([
page.locator('[href*="/event-types/"]').first().click(),
page.waitForNavigation({
url: (url) => url.pathname.startsWith("/event-types/"),
}),
page.waitForURL((url) => url.pathname.startsWith("/event-types/")),
]);
});
+6 -12
View File
@@ -94,10 +94,8 @@ test.describe("Event Types tests", () => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await page.waitForNavigation({
url: (url) => {
return !!url.pathname.match(/\/event-types\/.+/);
},
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
await page.locator("[data-testid=update-eventtype]").click();
const toast = await page.waitForSelector("div[class*='data-testid-toast-success']");
@@ -108,10 +106,8 @@ test.describe("Event Types tests", () => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await page.waitForNavigation({
url: (url) => {
return !!url.pathname.match(/\/event-types\/.+/);
},
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
const locationData = ["location 1", "location 2", "location 3"];
@@ -153,10 +149,8 @@ test.describe("Event Types tests", () => {
// to change the test as little as possible.
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant === "old-booker") {
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/book");
},
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
+4 -3
View File
@@ -1,11 +1,12 @@
import type { Page, WorkerInfo } from "@playwright/test";
import type Prisma from "@prisma/client";
import { MembershipRole, Prisma as PrismaType } from "@prisma/client";
import { Prisma as PrismaType } from "@prisma/client";
import { hashSync as hash } from "bcryptjs";
import dayjs from "@calcom/dayjs";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TimeZoneEnum } from "./types";
@@ -345,12 +346,12 @@ export async function getPaymentCredential(page: Page) {
/** We start the Stripe flow */
await Promise.all([
page.waitForNavigation({ url: "https://connect.stripe.com/oauth/v2/authorize?*" }),
page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"),
page.click('[data-testid="install-app-button"]'),
]);
await Promise.all([
page.waitForNavigation({ url: "/apps/installed/payment?hl=stripe" }),
page.waitForURL("/apps/installed/payment?hl=stripe"),
/** We skip filling Stripe forms (testing mode only) */
page.click('[id="skip-account-app"]'),
]);
@@ -49,7 +49,7 @@ test.describe("Stripe integration", () => {
await page.fill('[name="name"]', "Stripe Stripeson");
await page.fill('[name="email"]', "test@example.com");
await Promise.all([page.waitForNavigation({ url: "/payment/*" }), page.press('[name="email"]', "Enter")]);
await Promise.all([page.waitForURL("/payment/*"), page.press('[name="email"]', "Enter")]);
const stripeFrame = page
.frameLocator('iframe[src^="https://js.stripe.com/v3/elements-inner-card-"]')
+4 -8
View File
@@ -114,10 +114,8 @@ async function bookEventOnThisPage(page: Page) {
await bookTimeSlot(page);
// Make sure we're navigated to the success page
await page.waitForNavigation({
url(url) {
return url.pathname.startsWith("/booking");
},
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
}
@@ -156,10 +154,8 @@ export const createNewEventType = async (page: Page, args: { eventTitle: string
await page.fill("[name=length]", "10");
await page.click("[type=submit]");
await page.waitForNavigation({
url(url) {
return url.pathname !== "/event-types";
},
await page.waitForURL((url) => {
return url.pathname !== "/event-types";
});
};
@@ -1,10 +1,10 @@
import type { Locator, Page, PlaywrightTestArgs } from "@playwright/test";
import { expect } from "@playwright/test";
import { WebhookTriggerEvents } from "@prisma/client";
import type { createUsersFixture } from "playwright/fixtures/users";
import { uuid } from "short-uuid";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
@@ -362,8 +362,8 @@ async function rescheduleFromTheLinkOnPage({
await page.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(page);
if (bookerVariant === "old-booker") {
await page.waitForNavigation({
url: (url) => url.pathname.endsWith("/book"),
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
await page.click('[data-testid="confirm-reschedule-button"]');
@@ -380,8 +380,8 @@ async function openBookingFormInPreviewTab(
await previewTabPage.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(previewTabPage);
if (bookerVariant === "old-booker") {
await previewTabPage.waitForNavigation({
url: (url) => url.pathname.endsWith("/book"),
await previewTabPage.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
return previewTabPage;
+3 -5
View File
@@ -1,7 +1,7 @@
import { expect } from "@playwright/test";
import { BookingStatus } from "@prisma/client";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
@@ -149,10 +149,8 @@ testBothBookers.describe("Reschedule Tests", async () => {
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await page.waitForNavigation({
url(url) {
return url.pathname.indexOf("/payment") > -1;
},
await page.waitForURL((url) => {
return url.pathname.indexOf("/payment") > -1;
});
await expect(page).toHaveURL(/.*payment/);
+1 -3
View File
@@ -24,9 +24,7 @@ test("add webhook & test that creating an event triggers a webhook call", async
await Promise.all([
page.click("[type=submit]"),
page.waitForNavigation({
url: (url) => url.pathname.endsWith("/settings/developer/webhooks"),
}),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
@@ -1800,5 +1800,13 @@
"payment_app_commission": "Require payment ({{paymentFeePercentage}}% + {{fee, currency}} commission per transaction)",
"email_invite_team": "{{email}} has been invited",
"error_collecting_card": "Error collecting card",
"image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit"
"image_size_limit_exceed": "Uploaded image shouldn't exceed 5mb size limit",
"inline_embed": "Inline Embed",
"load_inline_content": "Loads your event type directly inline with your other website content.",
"floating_pop_up_button": "Floating pop-up button",
"floating_button_trigger_modal": "Puts a floating button on your site that triggers a modal with your event type.",
"pop_up_element_click": "Pop up via element click",
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
"book_my_cal": "Book my Cal"
}
@@ -599,7 +599,7 @@
"count_members_one": "{{count}} membro",
"count_members_other": "{{count}} membros",
"no_assigned_members": "Sem membros atribuídos",
"assigned_to": "Atribuído a",
"assigned_to": "Asignado a",
"start_assigning_members_above": "Comece a atribuir membros acima",
"locked_fields_admin_description": "Os membros não poderão editar isto",
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipa",
+2 -2
View File
@@ -7,14 +7,14 @@ import type {
User as PrismaUser,
Booking as PrismaBooking,
App as PrismaApp,
SchedulingType,
} from "@prisma/client";
import { diff } from "jest-diff";
import { v4 as uuidv4 } from "uuid";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { BookingStatus } from "@calcom/prisma/enums";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
@@ -25,6 +25,7 @@ import { appKeysSchema as event_type_app_card_zod_ts } from "./templates/event-t
import { appKeysSchema as vital_zod_ts } from "./vital/zod";
import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod";
import { appKeysSchema as zapier_zod_ts } from "./zapier/zod";
import { appKeysSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod";
import { appKeysSchema as zohocrm_zod_ts } from "./zohocrm/zod";
import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
@@ -52,6 +53,7 @@ export const appKeysSchemas = {
vital: vital_zod_ts,
wordpress: wordpress_zod_ts,
zapier: zapier_zod_ts,
"zoho-bigin": zoho_bigin_zod_ts,
zohocrm: zohocrm_zod_ts,
zoomvideo: zoomvideo_zod_ts,
};
@@ -60,6 +60,7 @@ import whereby_config_json from "./whereby/config.json";
import { metadata as wipemycalother__metadata_ts } from "./wipemycalother/_metadata";
import wordpress_config_json from "./wordpress/config.json";
import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
import zoho_bigin_config_json from "./zoho-bigin/config.json";
import zohocrm_config_json from "./zohocrm/config.json";
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
@@ -122,6 +123,7 @@ export const appStoreMetadata = {
wipemycalother: wipemycalother__metadata_ts,
wordpress: wordpress_config_json,
zapier: zapier__metadata_ts,
"zoho-bigin": zoho_bigin_config_json,
zohocrm: zohocrm_config_json,
zoomvideo: zoomvideo__metadata_ts,
};
@@ -25,6 +25,7 @@ import { appDataSchema as event_type_app_card_zod_ts } from "./templates/event-t
import { appDataSchema as vital_zod_ts } from "./vital/zod";
import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod";
import { appDataSchema as zapier_zod_ts } from "./zapier/zod";
import { appDataSchema as zoho_bigin_zod_ts } from "./zoho-bigin/zod";
import { appDataSchema as zohocrm_zod_ts } from "./zohocrm/zod";
import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
@@ -52,6 +53,7 @@ export const appDataSchemas = {
vital: vital_zod_ts,
wordpress: wordpress_zod_ts,
zapier: zapier_zod_ts,
"zoho-bigin": zoho_bigin_zod_ts,
zohocrm: zohocrm_zod_ts,
zoomvideo: zoomvideo_zod_ts,
};
@@ -60,6 +60,7 @@ export const apiHandlers = {
wipemycalother: import("./wipemycalother/api"),
wordpress: import("./wordpress/api"),
zapier: import("./zapier/api"),
"zoho-bigin": import("./zoho-bigin/api"),
zohocrm: import("./zohocrm/api"),
zoomvideo: import("./zoomvideo/api"),
};
+1 -1
View File
@@ -15,7 +15,7 @@
"label": "{TITLE}",
"linkType": "static",
"organizerInputPlaceholder": "https://discord.gg/420gg69",
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?(discord.gg|discord.com)\\/[a-zA-Z0-9]*"
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?discord\\.(gg|com)\\/[a-zA-Z0-9]+"
}
},
"description": "Copy your server invite link and start scheduling calls in Discord! Discord is a VoIP and instant messaging social platform. Users have the ability to communicate with voice calls, video calls, text messaging, media and files in private chats or as part of communities.",
+1
View File
@@ -15,6 +15,7 @@ const appStore = {
plausible: import("./plausible"),
salesforce: import("./salesforce"),
zohocrm: import("./zohocrm"),
"zoho-bigin": import("./zoho-bigin"),
sendgrid: import("./sendgrid"),
stripepayment: import("./stripepayment"),
tandemvideo: import("./tandemvideo"),
+1 -1
View File
@@ -1,8 +1,8 @@
import { BookingStatus } from "@prisma/client";
import type { TFunction } from "next-i18next";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import logger from "@calcom/lib/logger";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Ensure, Optional } from "@calcom/types/utils";
import type { EventLocationTypeFromAppMeta } from "../types/App";
@@ -130,9 +130,7 @@ test.describe("Routing Forms", () => {
// Install app
await page.goto(`/apps/routing-forms`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/routing-forms/forms`,
});
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
});
test.afterEach(async ({ users }) => {
@@ -163,9 +161,7 @@ test.describe("Routing Forms", () => {
// Install app
await page.goto(`/apps/routing-forms`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/routing-forms/forms`,
});
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
return user;
};
@@ -260,17 +256,13 @@ test.describe("Routing Forms", () => {
// Router should be publicly accessible
await users.logout();
page.goto(`/router?form=${routingForm.id}&Test field=event-routing`);
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/pro/30min");
},
await page.waitForURL((url) => {
return url.pathname.endsWith("/pro/30min");
});
page.goto(`/router?form=${routingForm.id}&Test field=external-redirect`);
await page.waitForNavigation({
url(url) {
return url.hostname.includes("google.com");
},
await page.waitForURL((url) => {
return url.hostname.includes("google.com");
});
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
@@ -352,19 +344,15 @@ async function fillSeededForm(page: Page, routingFormId: string) {
await gotoRoutingLink(page, routingFormId);
await page.fill('[data-testid="form-field"]', "event-routing");
page.click('button[type="submit"]');
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/pro/30min");
},
await page.waitForURL((url) => {
return url.pathname.endsWith("/pro/30min");
});
await gotoRoutingLink(page, routingFormId);
await page.fill('[data-testid="form-field"]', "external-redirect");
page.click('button[type="submit"]');
await page.waitForNavigation({
url(url) {
return url.hostname.includes("google.com");
},
await page.waitForURL((url) => {
return url.hostname.includes("google.com");
});
await gotoRoutingLink(page, routingFormId);
@@ -1,10 +1,11 @@
import type { App_RoutingForms_Form, User } from "@prisma/client";
import { Prisma, WebhookTriggerEvents } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@calcom/trpc/server";
import { authedProcedure, publicProcedure, router } from "@calcom/trpc/server/trpc";
@@ -19,14 +19,10 @@ const installApps = async (page: Page, users: Fixtures["users"]) => {
await user.login();
await page.goto(`/apps/routing-forms`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/routing-forms/forms`,
});
await page.waitForURL((url) => url.pathname === `/apps/routing-forms/forms`);
await page.goto(`/apps/typeform`);
await page.click('[data-testid="install-app-button"]');
await page.waitForNavigation({
url: (url) => url.pathname === `/apps/typeform/how-to-use`,
});
await page.waitForURL((url) => url.pathname === `/apps/typeform/how-to-use`);
};
test.describe("Typeform App", () => {
+1 -1
View File
@@ -1,5 +1,4 @@
import type { Prisma } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import queue from "queue";
@@ -9,6 +8,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { Reschedule } from "../lib";
import { initVitalClient, vitalEnv } from "../lib/client";
+1 -1
View File
@@ -1,5 +1,4 @@
import type { Booking, BookingReference, User } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import type { TFunction } from "next-i18next";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
@@ -10,6 +9,7 @@ import { sendRequestRescheduleEmail } from "@calcom/emails";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Person } from "@calcom/types/Calendar";
import { getCalendar } from "../../_utils/getCalendar";
@@ -1,9 +1,9 @@
import { BookingStatus } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import queue from "queue";
import { z, ZodError } from "zod";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { Reschedule } from "../lib";
@@ -1,5 +1,4 @@
import type { Booking, BookingReference, User } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import type { TFunction } from "next-i18next";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
@@ -10,6 +9,7 @@ import { sendRequestRescheduleEmail } from "@calcom/emails";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Person } from "@calcom/types/Calendar";
import { getCalendar } from "../../_utils/getCalendar";
@@ -1,4 +1,3 @@
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 } from "uuid";
@@ -6,6 +5,7 @@ import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
@@ -1,10 +1,10 @@
import { WebhookTriggerEvents } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
const querySchema = z.object({
apiKey: z.string(),
@@ -0,0 +1,6 @@
---
items:
- 1.png
---
{DESCRIPTION}
+9
View File
@@ -0,0 +1,9 @@
### Obtaining Zoho Bigin Client ID and Secret
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
2. Click "ADD CLIENT" button top right and select "Server-based Applications".
3. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoho-bigin/callback` replacing Cal.com URL with the URI at which your application runs.
4. Go to tab "Client Secret" tab.
5. Now copy the Client ID and Client Secret to your .env.appStore file into the `ZOHO_BIGIN_CLIENT_ID` and `ZOHO_BIGIN_CLIENT_SECRET` fields.
6. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
7. You're good to go. Now you can easily add Zoho Bigin from the Cal.com app store.
+32
View File
@@ -0,0 +1,32 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import { WEBAPP_URL } from "@calcom/lib/constants";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import appConfig from "../config.json";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") {
const appKeys = await getAppKeysFromSlug(appConfig.slug);
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
const redirectUri = WEBAPP_URL + `/api/integrations/${appConfig.slug}/callback`;
const authUrl = axios.getUri({
url: "https://accounts.zoho.com/oauth/v2/auth",
params: {
scope: appConfig.scope,
client_id: clientId,
response_type: "code",
redirect_uri: redirectUri,
access_type: "offline",
},
});
res.status(200).json({ url: authUrl });
return;
}
res.status(400).json({ message: "Invalid request method." });
}
@@ -0,0 +1,69 @@
import axios from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import qs from "qs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import prisma from "@calcom/prisma";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import appConfig from "../config.json";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { code, "accounts-server": accountsServer } = req.query;
if (code && typeof code !== "string") {
res.status(400).json({ message: "`code` must be a string" });
return;
}
if (!req.session?.user?.id) {
res.status(401).json({ message: "You must be logged in to do this" });
return;
}
const appKeys = await getAppKeysFromSlug(appConfig.slug);
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
const clientSecret = typeof appKeys.client_secret === "string" ? appKeys.client_secret : "";
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
if (!clientSecret) return res.status(400).json({ message: "Zoho Bigin client_secret missing." });
const accountsUrl = `${accountsServer}/oauth/v2/token`;
const redirectUri = WEBAPP_URL + `/api/integrations/${appConfig.slug}/callback`;
const formData = {
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: redirectUri,
grant_type: "authorization_code",
};
const tokenInfo = await axios.post(accountsUrl, qs.stringify(formData), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
});
tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
tokenInfo.data.accountServer = accountsServer;
await prisma.credential.create({
data: {
type: appConfig.type,
key: tokenInfo.data,
userId: req.session.user.id,
appId: appConfig.slug,
},
});
const state = decodeOAuthState(req);
res.redirect(
getSafeRedirectUrl(state?.returnTo) ??
getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug })
);
}
@@ -0,0 +1,2 @@
export { default as add } from "./add";
export { default as callback } from "./callback";
+17
View File
@@ -0,0 +1,17 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Zoho Bigin",
"slug": "zoho-bigin",
"type": "zoho-bigin_other_calendar",
"logo": "zohobigin.svg",
"url": "https://cal.com/apps/zoho-bigin",
"variant": "other",
"categories": ["other"],
"publisher": "Cal.com",
"email": "help@cal.com",
"description": "Bigin easily transforms your day-to-day customer processes into actionable pipelines. From qualifying leads to closing deals to managing important after-sales operations—Bigin connects your different teams to work together so that you can offer the best possible experience to your customers. Say goodbye to missing follow-ups, manual data entry, lack of team communication, and information silos.",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"scope": "ZohoBigin.modules.events.ALL,ZohoBigin.modules.contacts.ALL"
}
+2
View File
@@ -0,0 +1,2 @@
export * as api from "./api";
export * as lib from "./lib";
@@ -0,0 +1,312 @@
import axios from "axios";
import qs from "qs";
import { getLocation } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type {
Calendar,
CalendarEvent,
EventBusyDate,
IntegrationCalendar,
NewCalendarEventType,
Person,
} from "@calcom/types/Calendar";
import type { CredentialPayload } from "@calcom/types/Credential";
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
import { appKeysSchema } from "../zod";
export type BiginToken = {
scope: string;
api_domain: string;
expires_in: number;
expiryDate: number;
token_type: string;
access_token: string;
accountServer: string;
refresh_token: string;
};
export type BiginContact = {
email: string;
};
export default class BiginCalendarService implements Calendar {
private readonly integrationName = "zoho-bigin";
private readonly auth: { getToken: () => Promise<BiginToken> };
private log: typeof logger;
private eventsSlug = "/bigin/v1/Events";
private contactsSlug = "/bigin/v1/Contacts";
constructor(credential: CredentialPayload) {
this.auth = this.biginAuth(credential);
this.log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
}
/***
* Authenticate calendar service with Zoho Bigin provided credentials.
*/
private biginAuth(credential: CredentialPayload) {
const credentialKey = credential.key as unknown as BiginToken;
const credentialId = credential.id;
const isTokenValid = (token: BiginToken) =>
token.access_token && token.expiryDate && token.expiryDate > Date.now();
return {
getToken: () =>
isTokenValid(credentialKey)
? Promise.resolve(credentialKey)
: this.refreshAccessToken(credentialId, credentialKey),
};
}
/***
* Fetches a new access token if stored token is expired.
*/
private async refreshAccessToken(credentialId: number, credentialKey: BiginToken) {
this.log.debug("Refreshing token as it's invalid");
const grantType = "refresh_token";
const accountsUrl = `${credentialKey.accountServer}/oauth/v2/token`;
const appKeys = await getAppKeysFromSlug(this.integrationName);
const { client_id: clientId, client_secret: clientSecret } = appKeysSchema.parse(appKeys);
const formData = {
grant_type: grantType,
client_id: clientId,
client_secret: clientSecret,
refresh_token: credentialKey.refresh_token,
};
const tokenInfo = await axios.post(accountsUrl, qs.stringify(formData), {
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
},
});
if (!tokenInfo.data.error) {
// set expiry date as offset from current time.
tokenInfo.data.expiryDate = Math.round(Date.now() + tokenInfo.data.expires_in);
tokenInfo.data.accountServer = credentialKey.accountServer;
tokenInfo.data.refresh_token = credentialKey.refresh_token;
await prisma.credential.update({
where: {
id: credentialId,
},
data: {
key: tokenInfo.data as BiginToken,
},
});
this.log.debug("Fetched token", tokenInfo.data.access_token);
} else {
this.log.error(tokenInfo.data);
}
return tokenInfo.data as BiginToken;
}
/***
* Creates Zoho Bigin Contact records for every attendee added in event bookings.
* Returns the results of all contact creation operations.
*/
private async createContacts(attendees: Person[]) {
const token = await this.auth.getToken();
const contacts = attendees.map((attendee) => {
const nameParts = attendee.name.split(" ");
const firstName = nameParts[0];
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(" ") : "-";
return {
First_Name: firstName,
Last_Name: lastName,
Email: attendee.email,
};
});
return axios({
method: "post",
url: token.api_domain + this.contactsSlug,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${token.access_token}`,
},
data: JSON.stringify({ data: contacts }),
});
}
/***
* Finds existing Zoho Bigin Contact record based on email address. Returns a list of contacts objects that matched.
*/
private async contactSearch(event: CalendarEvent) {
const token = await this.auth.getToken();
const searchCriteria =
"(" + event.attendees.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`).join("or") + ")";
return await axios({
method: "get",
url: `${token.api_domain}${this.contactsSlug}/search?criteria=${searchCriteria}`,
headers: {
authorization: `Zoho-oauthtoken ${token.access_token}`,
},
})
.then((data) => data.data)
.catch((e) => this.log.error("Error searching contact:", JSON.stringify(e), e.response?.data));
}
/***
* Sends request to Zoho Bigin API to add new Events.
*/
private async createBiginEvent(event: CalendarEvent) {
const token = await this.auth.getToken();
const biginEvent = {
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: event.additionalNotes,
Location: getLocation(event),
};
return axios({
method: "post",
url: token.api_domain + this.eventsSlug,
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${token.access_token}`,
},
data: JSON.stringify({ data: [biginEvent] }),
})
.then((data) => data.data)
.catch((e) => this.log.error("Error creating bigin event", JSON.stringify(e), e.response?.data));
}
/***
* Handles orchestrating the creation of new events in Zoho Bigin.
*/
async handleEventCreation(event: CalendarEvent, contacts: CalendarEvent["attendees"]) {
const meetingEvent = await this.createBiginEvent(event);
if (meetingEvent.data && meetingEvent.data.length && meetingEvent.data[0].status === "success") {
this.log.debug("event:creation:ok", { meetingEvent });
return Promise.resolve({
uid: meetingEvent.data[0].details.id,
id: meetingEvent.data[0].details.id,
//FIXME: `externalCalendarId` is required by the `updateAllCalendarEvents` method, but is not used by zoho-bigin App. Not setting this property actually skips calling updateEvent..
// Here the value doesn't matter. We just need to set it to something.
externalCalendarId: "NO_CALENDAR_ID_NEEDED",
type: this.integrationName,
password: "",
url: "",
additionalInfo: { contacts, meetingEvent },
});
}
this.log.debug("meeting:creation:notOk", { meetingEvent, event, contacts });
return Promise.reject("Something went wrong when creating a meeting in Zoho Bigin");
}
/***
* Creates contacts and event records for new bookings.
* Initially creates all new attendees as contacts, then creates the event.
*/
async createEvent(event: CalendarEvent): Promise<NewCalendarEventType> {
const contacts = (await this.contactSearch(event))?.data || [];
const existingContacts = contacts.map((contact: BiginContact) => contact.email);
const newContacts: Person[] = event.attendees.filter(
(attendee) => !existingContacts.includes(attendee.email)
);
if (newContacts.length === 0) {
return await this.handleEventCreation(event, event.attendees);
}
const createContacts = await this.createContacts(newContacts);
if (createContacts.data?.data[0].status === "success") {
return await this.handleEventCreation(event, event.attendees);
}
return Promise.reject({
calError: "Something went wrong when creating non-existing attendees in Zoho Bigin",
});
}
/***
* Updates an existing event in Zoho Bigin.
*/
async updateEvent(uid: string, event: CalendarEvent): Promise<NewCalendarEventType> {
this.log.debug(`Updating Event with uid ${uid}`);
const token = await this.auth.getToken();
const biginEvent = {
id: uid,
Event_Title: event.title,
Start_DateTime: toISO8601String(new Date(event.startTime)),
End_DateTime: toISO8601String(new Date(event.endTime)),
Description: event.additionalNotes,
Location: getLocation(event),
};
return axios
.put(token.api_domain + this.eventsSlug, JSON.stringify({ data: [biginEvent] }), {
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${token.access_token}`,
},
})
.then((data) => data.data)
.catch((e) => {
this.log.error("Error in updating bigin event", JSON.stringify(e), e.response?.data);
});
}
async deleteEvent(uid: string): Promise<void> {
const token = await this.auth.getToken();
return axios
.delete(`${token.api_domain}${this.eventsSlug}?ids=${uid}`, {
headers: {
"content-type": "application/json",
authorization: `Zoho-oauthtoken ${token.access_token}`,
},
})
.then((data) => data.data)
.catch((e) => this.log.error("Error deleting bigin event", JSON.stringify(e), e.response?.data));
}
async getAvailability(
_dateFrom: string,
_dateTo: string,
_selectedCalendars: IntegrationCalendar[]
): Promise<EventBusyDate[]> {
return Promise.resolve([]);
}
async listCalendars(_event?: CalendarEvent): Promise<IntegrationCalendar[]> {
return Promise.resolve([]);
}
}
const toISO8601String = (date: Date) => {
const tzo = -date.getTimezoneOffset(),
dif = tzo >= 0 ? "+" : "-",
pad = function (num: number) {
return (num < 10 ? "0" : "") + num;
};
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
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)
);
};
@@ -0,0 +1 @@
export { default as CalendarService } from "./CalendarService";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/zoho-bigin",
"version": "1.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "Bigin easily transforms your day-to-day customer processes into actionable pipelines. From qualifying leads to closing deals to managing important after-sales operations—Bigin connects your different teams to work together so that you can offer the best possible experience to your customers. Say goodbye to missing follow-ups, manual data entry, lack of team communication, and information silos."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="b" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" width="39.629" height="44" viewBox="0 0 39.629 44">
<defs>
<style>
.d {
fill: #039649;
}
</style>
</defs>
<g id="c" data-name="Layer 1">
<path class="d" d="m19.21,44c-.66,0-1.32-.22-1.87-.55-.77-.55-1.32-1.54-1.32-2.53v-9.02L.4,4.73C-.15,3.63-.15,2.42.51,1.54c.55-.99,1.65-1.54,2.75-1.54h28.6c1.099,0,2.089.55,2.639,1.54.66.991.66,2.31.11,3.301l-1.87,3.3h3.74c1.1,0,2.089.55,2.639,1.54.66.99.66,2.31.11,3.3l-10.89,18.81v6.82c0,1.43-.88,2.53-2.2,2.97l-5.94,2.31c-.22.11-.66.11-.99.11M3.26,3.08q-.11.11-.11.221l15.51,27.059c.44.77.44,1.21.44,1.65v8.799s.11.11.22,0l5.94-2.31h.22v-6.6c0-.44,0-1.21.441-1.76l10.78-18.7c0-.11,0-.22-.11-.22l-19.69-.11,5.28,9.13,3.74-6.49c.44-.77,1.32-.99,2.09-.55.77.44.99,1.32.549,2.09l-4.399,7.59c-.991,1.54-3.19,1.43-4.071.11l-6.38-11.33c-.44-.66-.44-1.54,0-2.31.441-.77,1.211-1.32,2.09-1.32h13.42l2.75-4.73s0-.11-.11-.221H3.26Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
export const appKeysSchema = z.object({
client_id: z.string().min(1),
client_secret: z.string().min(1),
});
export const appDataSchema = z.object({});
+1 -1
View File
@@ -280,7 +280,7 @@ export const createEvent = async (
if (error?.calError) {
calError = error.calError;
}
log.error("createEvent failed", error, calEvent);
log.error("createEvent failed", JSON.stringify(error), calEvent);
// @TODO: This code will be off till we can investigate an error with it
//https://github.com/calcom/cal.com/issues/3949
// await sendBrokenIntegrationEmail(calEvent, "calendar");
+1 -2
View File
@@ -1,5 +1,4 @@
import { SchedulingType } from "@prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import type { WorkingHours } from "@calcom/types/schedule";
/**
+1 -1
View File
@@ -1,5 +1,4 @@
import type { Credential } from "@prisma/client";
import { BookingStatus } from "@prisma/client";
import { getBusyCalendarTimes } from "@calcom/core/CalendarManager";
import dayjs from "@calcom/dayjs";
@@ -7,6 +6,7 @@ import logger from "@calcom/lib/logger";
import { performance } from "@calcom/lib/server/perfObserver";
import prisma from "@calcom/prisma";
import type { SelectedCalendar } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import type { EventBusyDetails } from "@calcom/types/Calendar";
export async function getBusyTimes(params: {
+3 -4
View File
@@ -1,5 +1,5 @@
import { decodeHTML } from "entities";
import nodemailer from "nodemailer";
import { createTransport } from "nodemailer";
import { z } from "zod";
import type { Dayjs } from "@calcom/dayjs";
@@ -52,9 +52,8 @@ export default class BaseEmail {
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
};
new Promise((resolve, reject) =>
nodemailer
.createTransport(this.getMailerOptions().transport)
new Promise((resolve, reject) =>
createTransport(this.getMailerOptions().transport)
.sendMail(payloadWithUnEscapedSubject, (_err, info) => {
if (_err) {
const err = getErrorFromUnknown(_err);
@@ -91,12 +91,11 @@ async function selectFirstAvailableTimeSlotNextMonth(frame: Frame, page: Page) {
}
export async function bookFirstEvent(username: string, frame: Frame, page: Page) {
// Click first event type
// Click first event type on Profile Page
await frame.click('[data-testid="event-type-link"]');
await frame.waitForNavigation({
url(url) {
return !!url.pathname.match(new RegExp(`/${username}/.*$`));
},
await frame.waitForURL((url) => {
// Wait for reaching the event page
return !!url.pathname.match(new RegExp(`/${username}/.+$`));
});
// Let current month dates fully render.
@@ -107,10 +106,8 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
// expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png");
const eventSlug = new URL(frame.url()).pathname;
await selectFirstAvailableTimeSlotNextMonth(frame, page);
await frame.waitForNavigation({
url(url) {
return url.pathname.includes(`/${username}/book`);
},
await frame.waitForURL((url) => {
return url.pathname.includes(`/${username}/book`);
});
// expect(await page.screenshot()).toMatchSnapshot("booking-page.png");
// --- fill form
@@ -136,10 +133,8 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
export async function rescheduleEvent(username, frame, page) {
await selectFirstAvailableTimeSlotNextMonth(frame, page);
await frame.waitForNavigation({
url(url) {
return url.pathname.includes(`/${username}/book`);
},
await frame.waitForURL((url) => {
return url.pathname.includes(`/${username}/book`);
});
// --- fill form
await frame.press('[name="email"]', "Enter");
+3 -3
View File
@@ -12,6 +12,9 @@
.cell-2 {
margin: 10px;
}
.dark {
background-color:rgb(16 16 16);
}
</style>
<script>
const searchParams = new URL(document.URL).searchParams;
@@ -22,8 +25,5 @@
<script type="module" src="./src/preview.ts"></script>
<body>
<div id="my-embed" style="width: 100%; height: 90%; overflow: scroll"></div>
<script type="module">
</script>
</body>
</html>
+24
View File
@@ -90,3 +90,27 @@ previewWindow.addEventListener("message", (e) => {
}
}
});
function makePreviewPageUseSystemPreference() {
const colorSchemeQuery = window.matchMedia("(prefers-color-scheme: dark)");
function handleColorSchemeChange(e: MediaQueryListEvent) {
if (e.matches) {
// Dark color scheme
document.body.classList.remove("light");
document.body.classList.add("dark");
} else {
// Light color scheme
document.body.classList.add("light");
document.body.classList.remove("dark");
}
}
colorSchemeQuery.addEventListener("change", handleColorSchemeChange);
// Initial check
handleColorSchemeChange(new MediaQueryListEvent("change", { matches: colorSchemeQuery.matches }));
}
// This makes preview page behave like a website that has system preference enabled. This provides a better experience of preview when user switch their system theme to dark
makePreviewPageUseSystemPreference();
@@ -4,6 +4,7 @@ const recommended = {
rules: {
"@calcom/eslint/deprecated-imports": "error",
"@calcom/eslint/avoid-web-storage": "error",
"@calcom/eslint/avoid-prisma-client-import-for-enums": "error",
},
};
@@ -0,0 +1,45 @@
import { ESLintUtils } from "@typescript-eslint/utils";
const createRule = ESLintUtils.RuleCreator((name) => `https://developer.cal.com/eslint/rule/${name}`);
const rule = createRule({
create(context) {
return {
ImportDeclaration(node) {
node.source.value === "@prisma/client" &&
node.importKind !== "type" &&
node.specifiers.forEach((item) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const enumType = item.imported?.name; // ts doesn't know about imported, bad type?
if (!enumType || enumType === "Prisma" || enumType === "PrismaClient") return null;
return context.report({
node: item,
loc: node.loc,
messageId: "avoid-prisma-client-import",
data: {
enumType,
},
});
});
},
};
},
name: "avoid-prisma-client-import-for-enums",
meta: {
fixable: "code",
docs: {
description: "Avoid prisma client import for enums",
recommended: "error",
},
messages: {
"avoid-prisma-client-import": `Import { {{enumType}} } from '@calcom/prisma/enums' to avoid including @prisma/client.`,
},
type: "suggestion",
schema: [],
},
defaultOptions: [],
});
export default rule;
+1 -1
View File
@@ -2,7 +2,7 @@
import type { ESLint } from "eslint";
export default {
"my-first-rule": require("./my-first-rule").default,
"deprecated-imports": require("./deprecated-imports").default,
"avoid-web-storage": require("./avoid-web-storage").default,
"avoid-prisma-client-import-for-enums": require("./avoid-prisma-client-import-for-enums").default,
} as ESLint.Plugin["rules"];
+1 -1
View File
@@ -1,5 +1,4 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AppCategories } from "@prisma/client";
import { noop } from "lodash";
import { useRouter } from "next/router";
import type { FC } from "react";
@@ -11,6 +10,7 @@ import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavi
import { appKeysSchemas } from "@calcom/app-store/apps.keys-schemas.generated";
import { classNames as cs } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCategories } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import {
@@ -1,8 +1,9 @@
import { UserPermissionRole } from "@prisma/client";
import { useSession } from "next-auth/react";
import type { FC } from "react";
import { Fragment } from "react";
import { UserPermissionRole } from "@calcom/prisma/enums";
type AdminRequiredProps = {
as?: keyof JSX.IntrinsicElements;
children?: React.ReactNode;
@@ -1,4 +1,4 @@
import { IdentityProvider } from "@prisma/client";
import { IdentityProvider } from "@calcom/prisma/enums";
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
[IdentityProvider.CAL]: "Cal",
@@ -1,5 +1,4 @@
import type { UserPermissionRole, Membership, Team } from "@prisma/client";
import { IdentityProvider } from "@prisma/client";
import type { AuthOptions, Session } from "next-auth";
import { encode } from "next-auth/jwt";
import type { Provider } from "next-auth/providers";
@@ -18,6 +17,7 @@ import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { ErrorCode } from "./ErrorCode";
+22 -9
View File
@@ -1,4 +1,5 @@
import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
import dynamic from "next/dynamic";
import { useCallback, useEffect, useRef } from "react";
import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow";
@@ -7,14 +8,13 @@ import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { Logo, ToggleGroup, useCalcomTheme } from "@calcom/ui";
import { ToggleGroup, useCalcomTheme } from "@calcom/ui";
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
import { AvailableTimeSlots } from "./components/AvailableTimeSlots";
import { Away } from "./components/Away";
import { BookEventForm } from "./components/BookEventForm";
import { BookFormAsModal } from "./components/BookEventForm/BookFormAsModal";
import { DatePicker } from "./components/DatePicker";
import { EventMeta } from "./components/EventMeta";
import { LargeCalendar } from "./components/LargeCalendar";
import { LargeViewHeader } from "./components/LargeViewHeader";
@@ -24,6 +24,11 @@ import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerLayout, BookerProps } from "./types";
import { useEvent } from "./utils/event";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
ssr: false,
});
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
@@ -121,13 +126,18 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
<div
style={resizeAnimationConfig[layout]?.[bookerState] || resizeAnimationConfig[layout].default}
className={classNames(
// Size settings are abstracted on their own lines purely for readbility.
// General sizes:
"[--booker-main-width:480px] [--booker-timeslots-width:240px] lg:[--booker-timeslots-width:280px]",
"bg-muted grid max-w-full items-start overflow-clip dark:[color-scheme:dark] md:flex-row",
"bg-muted grid max-w-full auto-rows-max items-start overflow-clip dark:[color-scheme:dark] md:flex-row",
layout === "small_calendar" &&
"border-subtle mt-20 min-h-[450px] w-[calc(var(--booker-meta-width)+var(--booker-main-width))] rounded-md border [--booker-meta-width:280px]",
layout !== "small_calendar" &&
"h-auto min-h-screen w-screen [--booker-meta-width:340px] lg:[--booker-meta-width:424px]",
"transition-[width] duration-300"
"w-[calc(var(--booker-meta-width)+var(--booker-main-width))] [--booker-meta-width:240px] lg:[--booker-meta-width:280px]",
// Sizes for fullscreen layouts:
layout !== "small_calendar" && "[--booker-meta-width:340px] lg:[--booker-meta-width:424px]",
// Other styles
layout === "small_calendar" && "border-subtle mt-20 min-h-[450px] rounded-md border",
layout !== "small_calendar" && "h-auto min-h-screen w-screen",
"transition-[width,max-width] duration-300"
)}>
<AnimatePresence>
<StickyOnDesktop key="meta" className="relative z-10">
@@ -199,8 +209,11 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
<m.span
key="logo"
className={classNames("mt-auto mb-6 pt-6", layout === "small_calendar" ? "block" : "hidden")}>
<Logo small />
className={classNames(
"mt-auto mb-6 pt-6 [&_img]:h-[15px]",
layout === "small_calendar" ? "block" : "hidden"
)}>
<PoweredBy logoOnly />
</m.span>
</div>
@@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
@@ -12,7 +11,6 @@ import { useBookerStore } from "../store";
import { useEvent, useScheduleForEvent } from "../utils/event";
export const DatePicker = () => {
const [isLoadedClientSide, setIsLoadedClientSide] = useState(false);
const { i18n } = useLocale();
const [month, selectedDate] = useBookerStore((state) => [state.month, state.selectedDate], shallow);
const [setSelectedDate, setMonth] = useBookerStore(
@@ -23,15 +21,6 @@ export const DatePicker = () => {
const schedule = useScheduleForEvent();
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
// Not rendering the component on the server side to avoid hydration issues
// @TODO: We should update the datepicker component as soon as the current booker isn't
// used anymore, so we don't need to have this check.
useEffect(() => {
setIsLoadedClientSide(true);
}, []);
if (!isLoadedClientSide) return null;
return (
<DatePickerComponent
isLoading={schedule.isLoading}
+3 -2
View File
@@ -58,9 +58,10 @@ export const resizeAnimationConfig: ResizeAnimationConfig = {
gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width)",
},
selecting_time: {
width: "calc(var(--booker-meta-width) + var(--booker-main-width) + var(--booker-timeslots-width))",
width: "100%",
maxWidth: "calc(var(--booker-meta-width) + var(--booker-main-width) + var(--booker-timeslots-width))",
gridTemplateAreas: `"meta main timeslots"`,
gridTemplateColumns: "var(--booker-meta-width) var(--booker-main-width) var(--booker-timeslots-width)",
gridTemplateColumns: "var(--booker-meta-width) 1fr var(--booker-timeslots-width)",
},
},
large_calendar: {
@@ -1,8 +1,8 @@
import { CAL_URL } from "@calcom/lib/constants";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { SchedulingType } from "@calcom/prisma/enums";
import { AvatarGroup } from "@calcom/ui";
import type { PublicEvent } from "../../types";
import { SchedulingType } from ".prisma/client";
export interface EventMembersProps {
/**
@@ -21,7 +21,7 @@ export const EventMembers = ({ schedulingType, users, profile }: EventMembersPro
const avatars = shownUsers
.map((user) => ({
title: `${user.name}`,
image: "image" in user ? `${user.image}` : `${CAL_URL}/${user.username}/avatar.png`,
image: "image" in user ? `${user.image}` : `${WEBAPP_URL}/${user.username}/avatar.png`,
alt: user.name || undefined,
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
}))
@@ -2,6 +2,7 @@ import type { EventTypeCustomInput, EventType, Prisma, Workflow } from "@prisma/
import { z } from "zod";
import slugify from "@calcom/lib/slugify";
import { EventTypeCustomInputType } from "@calcom/prisma/enums";
import {
BookingFieldType,
customInputSchema,
@@ -43,15 +44,6 @@ export const getSmsReminderNumberSource = ({
type Fields = z.infer<typeof eventTypeBookingFields>;
const EventTypeCustomInputType = {
TEXT: "TEXT",
TEXTLONG: "TEXTLONG",
NUMBER: "NUMBER",
BOOL: "BOOL",
RADIO: "RADIO",
PHONE: "PHONE",
} as const;
export const SystemField = z.enum([
"name",
"email",
@@ -1,5 +1,4 @@
import type { Prisma, WebhookTriggerEvents, WorkflowReminder } from "@prisma/client";
import { BookingStatus, MembershipRole, WorkflowMethods } from "@prisma/client";
import type { NextApiRequest } from "next";
import appStore from "@calcom/app-store";
@@ -22,6 +21,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { handleRefundError } from "@calcom/lib/payment/handleRefundError";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { BookingStatus, MembershipRole, WorkflowMethods } from "@calcom/prisma/enums";
import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -1,5 +1,4 @@
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import type { EventManagerUser } from "@calcom/core/EventManager";
@@ -10,6 +9,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -1,5 +1,5 @@
import type { App, Attendee, Credential, EventTypeCustomInput } from "@prisma/client";
import { BookingStatus, SchedulingType, WebhookTriggerEvents, WorkflowMethods, Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import async from "async";
import { isValidPhoneNumber } from "libphonenumber-js";
import { cloneDeep } from "lodash";
@@ -53,6 +53,7 @@ import { updateWebUser as syncServicesUpdateWebUser } from "@calcom/lib/sync/Syn
import { TimeFormat } from "@calcom/lib/timeFormat";
import prisma, { userSelect } from "@calcom/prisma";
import type { BookingReference } from "@calcom/prisma/client";
import { BookingStatus, SchedulingType, WebhookTriggerEvents, WorkflowMethods } from "@calcom/prisma/enums";
import { bookingCreateSchemaLegacyPropsForApi } from "@calcom/prisma/zod-utils";
import {
bookingCreateBodySchemaForApi,
@@ -64,7 +65,7 @@ import {
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { WorkingHours } from "@calcom/types/schedule";
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
import sendPayload from "../../webhooks/lib/sendPayload";
@@ -117,10 +118,14 @@ const isWithinAvailableHours = (
timeSlot: { start: ConfigType; end: ConfigType },
{
workingHours,
dateOverrides,
organizerTimeZone,
inviteeTimeZone,
}: {
workingHours: WorkingHours[];
dateOverrides: DateOverride[];
organizerTimeZone: string;
inviteeTimeZone: string;
}
) => {
const timeSlotStart = dayjs(timeSlot.start).utc();
@@ -144,6 +149,22 @@ const isWithinAvailableHours = (
return true;
}
}
// check if it is a date override
for (const dateOverride of dateOverrides) {
const utcOffSet = dayjs(dateOverride.start).tz(inviteeTimeZone).utcOffset();
const slotStart = dayjs(timeSlotStart).add(utcOffSet, "minute");
const slotEnd = dayjs(timeSlotEnd).add(utcOffSet, "minute");
if (
slotStart.isBetween(dateOverride.start, dateOverride.end) &&
slotEnd.isBetween(dateOverride.start, dateOverride.end)
) {
return true;
}
}
log.error(
`NAUF: isWithinAvailableHours ${JSON.stringify({ ...timeSlot, organizerTimeZone, workingHours })}`
);
@@ -302,7 +323,11 @@ async function ensureAvailableUsers(
const availableUsers: IsFixedAwareUser[] = [];
/** Let's start checking for availability */
for (const user of eventType.users) {
const { busy: bufferedBusyTimes, workingHours } = await getUserAvailability(
const {
busy: bufferedBusyTimes,
workingHours,
dateOverrides,
} = await getUserAvailability(
{
userId: user.id,
eventTypeId: eventType.id,
@@ -317,7 +342,9 @@ async function ensureAvailableUsers(
{ start: input.dateFrom, end: input.dateTo },
{
workingHours,
dateOverrides,
organizerTimeZone: eventType.timeZone || eventType?.schedule?.timeZone || user.timeZone,
inviteeTimeZone: input.timeZone,
}
)
) {

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