Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b83ee2d57d | ||
|
|
5f39bbe730 | ||
|
|
cf3d8cb4bb | ||
|
|
fbe0018d01 | ||
|
|
6586ed4d87 | ||
|
|
81e57afcf4 | ||
|
|
5cc530593b | ||
|
|
4111f78431 | ||
|
|
def80ea4cf | ||
|
|
2fc4676f6d | ||
|
|
318bafaab8 | ||
|
|
fc10c29037 | ||
|
|
ccb7b1f9ac | ||
|
|
c6c6544f50 | ||
|
|
8a6b93a747 | ||
|
|
ff6c6ccec0 | ||
|
|
c8db177f7d | ||
|
|
8871e1eb21 | ||
|
|
92b2d57871 | ||
|
|
77623c72c5 | ||
|
|
3239976e18 | ||
|
|
611939ffe8 | ||
|
|
1b8ecbe449 | ||
|
|
cd4882429d | ||
|
|
4fd2d80637 | ||
|
|
a343d934fe | ||
|
|
933cc739bd | ||
|
|
c7c75ff4d3 | ||
|
|
41f1920a7a | ||
|
|
e4836ea8f3 | ||
|
|
bfa30aa504 | ||
|
|
b36043b637 | ||
|
|
75026289fd | ||
|
|
80649c9f1a | ||
|
|
7dd75afb63 | ||
|
|
f66e4aeeed | ||
|
|
8225e61710 | ||
|
|
175fc4fcf3 | ||
|
|
f20a5df322 | ||
|
|
62544931b2 | ||
|
|
f6ad1d6ed5 | ||
|
|
d22b1072b5 | ||
|
|
4b356dd937 | ||
|
|
1f51199a7b | ||
|
|
b80a631998 | ||
|
|
bef4422080 | ||
|
|
cfd1a72c64 | ||
|
|
23b3a6661c | ||
|
|
b8b6c48d7d | ||
|
|
81655f9988 | ||
|
|
ff74822c48 | ||
|
|
8ec178debd | ||
|
|
f881061d8d | ||
|
|
2525559d6b | ||
|
|
38fb7a3f77 | ||
|
|
08956e54c5 | ||
|
|
ea65a2a27e | ||
|
|
0d45cfb7ea | ||
|
|
a4fc25e260 | ||
|
|
796c932311 | ||
|
|
4137f63ec7 | ||
|
|
2147cc82a5 | ||
|
|
40e646e9a4 | ||
|
|
cb2225259c | ||
|
|
372cd94d9f | ||
|
|
c7c282f30f | ||
|
|
959488910f | ||
|
|
0ca0cc3265 | ||
|
|
69bc12187e | ||
|
|
5e772530b2 | ||
|
|
3088b1174a | ||
|
|
fadf12b2bd | ||
|
|
c81f30b092 | ||
|
|
476119d627 | ||
|
|
040f03a9b5 | ||
|
|
b552bf6956 | ||
|
|
82d3c4370d | ||
|
|
492b4d3e97 | ||
|
|
0ae319d206 | ||
|
|
749311be2a | ||
|
|
3cce4e5009 | ||
|
|
341f82ccb9 | ||
|
|
e028a339f5 | ||
|
|
c55e432bc3 | ||
|
|
f47c3cd538 | ||
|
|
4b70a20941 | ||
|
|
c873b83965 | ||
|
|
ff859737ec | ||
|
|
607ef71f9f | ||
|
|
24dd4002fa | ||
|
|
078c9ddca5 | ||
|
|
b44e5a7aae | ||
|
|
4374da08e2 | ||
|
|
98b531e6b5 | ||
|
|
74414db760 | ||
|
|
bdf3e34ea1 | ||
|
|
738e0f3e50 | ||
|
|
16ae19abdb | ||
|
|
a2e18ae9ba | ||
|
|
e2cfd0d1d1 | ||
|
|
067b77faf1 | ||
|
|
740c01ac29 | ||
|
|
97cfd1ffa7 | ||
|
|
a7b2cfc90f | ||
|
|
d067b0782a | ||
|
|
203a36e2b5 | ||
|
|
f5a38c3f89 | ||
|
|
f179c23cc5 | ||
|
|
9f83885e8c | ||
|
|
8ffb5529e6 | ||
|
|
2e686d03fa | ||
|
|
cc3be0a6ac | ||
|
|
ec3a3268c9 | ||
|
|
77eed5611e | ||
|
|
a827d1defb | ||
|
|
b08a62d30b | ||
|
|
d6fb0df64f | ||
|
|
03548d8381 | ||
|
|
36d8554fd5 | ||
|
|
6fc9e9315d | ||
|
|
6eb8119a82 | ||
|
|
f4c613e6d1 | ||
|
|
082507aea5 | ||
|
|
90c1270e3d | ||
|
|
94917dbb1e | ||
|
|
fbbcc401cb | ||
|
|
af7d7ec678 | ||
|
|
3b7d85852a | ||
|
|
5362cd66b4 | ||
|
|
2907405d4e | ||
|
|
3c46c39114 | ||
|
|
c6ab889b98 | ||
|
|
a2e2b0ed34 | ||
|
|
bc38b3bc55 | ||
|
|
7611d5c24e | ||
|
|
5d2fd7aafb | ||
|
|
b9ade55ac1 | ||
|
|
d7051c4a3f | ||
|
|
ab50067194 | ||
|
|
cdba1920fc | ||
|
|
98a3508b8f | ||
|
|
446f384405 | ||
|
|
be79b8ecd6 | ||
|
|
371e7b024a | ||
|
|
76807a7e70 | ||
|
|
d3f77bf292 | ||
|
|
2f175eacf5 | ||
|
|
b45aea3f26 | ||
|
|
c401a58bb1 | ||
|
|
05b42c001f | ||
|
|
20f7a5841e | ||
|
|
dbf6bb991f | ||
|
|
75cd5a05b8 | ||
|
|
ca6afed27c | ||
|
|
45250f6f3e | ||
|
|
bd5af9e821 | ||
|
|
4267c9063b | ||
|
|
2b49d96e4a | ||
|
|
127c96b46c | ||
|
|
3595ba9824 | ||
|
|
56acfa38f1 | ||
|
|
e78fb22451 | ||
|
|
99f736b317 | ||
|
|
ee9f80ae3b | ||
|
|
a3c283da5e | ||
|
|
0ef678b2cf | ||
|
|
4f466fd95d | ||
|
|
5672a721d9 | ||
|
|
5ad45900ea | ||
|
|
ada88a2f7e | ||
|
|
9ba90e18de | ||
|
|
a88eb0fdc5 | ||
|
|
4b4c1200cd | ||
|
|
1cea7f9760 | ||
|
|
28dcf69d18 | ||
|
|
83d5176ddb | ||
|
|
3815a4c4bf | ||
|
|
53748eb380 | ||
|
|
28af11f90e | ||
|
|
ea676907b2 | ||
|
|
221c8b75bc | ||
|
|
5b9b7ab22d | ||
|
|
668ea088ef | ||
|
|
8a6c420268 | ||
|
|
d2f8a5c076 | ||
|
|
9c0f7bbfc7 | ||
|
|
8297824051 | ||
|
|
7ff5b66f46 | ||
|
|
55570ed4bf | ||
|
|
51695ee79d | ||
|
|
543466f1b9 | ||
|
|
4abf2d67d9 | ||
|
|
1eeb91a793 | ||
|
|
a4725920ff | ||
|
|
4cc7766f88 | ||
|
|
0c78715e9a | ||
|
|
671d9b4903 | ||
|
|
524cefcbad | ||
|
|
72f72d86b2 | ||
|
|
c5c29322d3 | ||
|
|
863b04e9c1 | ||
|
|
c2833f1b44 | ||
|
|
25df264823 | ||
|
|
e870312ebe | ||
|
|
5371bff6ff | ||
|
|
c791344c8b | ||
|
|
5e149d9f5f | ||
|
|
3e53aa13eb | ||
|
|
317f961bc8 | ||
|
|
b0c57a5c38 | ||
|
|
39c16bebd5 | ||
|
|
b3a5f4a6c0 | ||
|
|
379785196d | ||
|
|
04f8de238f | ||
|
|
0af2be9d8f | ||
|
|
98d31157cb | ||
|
|
f99d518a06 | ||
|
|
62770ecc49 | ||
|
|
be05ec66ce | ||
|
|
8defc351be | ||
|
|
ab112d92ea | ||
|
|
cf1b124dc1 | ||
|
|
38158ee043 | ||
|
|
8d214cd8ac | ||
|
|
01ed11ae33 | ||
|
|
5c7756ce6b | ||
|
|
a8c5227536 | ||
|
|
f3730daf05 | ||
|
|
47d5d800b0 | ||
|
|
f6d00a41fb | ||
|
|
692f2778e4 | ||
|
|
81d8628a87 | ||
|
|
07fbc4cbd3 | ||
|
|
8cb9a96c3a | ||
|
|
73b1c1dfb7 | ||
|
|
224a8864a6 | ||
|
|
6d02ac6729 | ||
|
|
f838e8bd53 | ||
|
|
5dac6896d2 | ||
|
|
496cb265d1 | ||
|
|
c037a62a9d | ||
|
|
88d17165ec | ||
|
|
0e7e1c2cc0 | ||
|
|
eb97feb173 | ||
|
|
7d702dd546 | ||
|
|
82b6b83885 | ||
|
|
39b6def444 | ||
|
|
7f5cdbab9b | ||
|
|
1770b832f4 | ||
|
|
d30c0a626d | ||
|
|
2eed750ef6 | ||
|
|
dd8968d0e7 | ||
|
|
827bebbc31 | ||
|
|
d97ac0e0f8 | ||
|
|
600d2725aa | ||
|
|
2a09dc6a46 | ||
|
|
1f746b67f4 | ||
|
|
bb95f2ba39 | ||
|
|
3d93b4127c | ||
|
|
094fae38fc | ||
|
|
3af614f778 | ||
|
|
77b41a4623 | ||
|
|
c23738b885 | ||
|
|
afe1f5b72f | ||
|
|
5ce341093b | ||
|
|
fc6d2839fb | ||
|
|
2fadba85f4 | ||
|
|
473b36a6be | ||
|
|
f74b9eb70a | ||
|
|
a845841ad0 | ||
|
|
dba80a1b64 | ||
|
|
41558d297c | ||
|
|
cd3946d482 | ||
|
|
dcbf695217 | ||
|
|
49cbdda4cf | ||
|
|
22f458cb74 | ||
|
|
561d9f154b | ||
|
|
0c9b9bc296 | ||
|
|
552bfcc7db | ||
|
|
986f9dca8d | ||
|
|
3af5a7404e | ||
|
|
10d3c6ecaa | ||
|
|
a0c37ea4b7 | ||
|
|
798999ad1a | ||
|
|
d40e306baa | ||
|
|
e558e61f6b | ||
|
|
210eafcd6e | ||
|
|
c05e7d75a5 | ||
|
|
d0ebf3319a | ||
|
|
9eb4187644 | ||
|
|
958cebc82c | ||
|
|
0c36dcac4c | ||
|
|
fe996b4e41 | ||
|
|
c1c95f711b | ||
|
|
7cb7006c54 | ||
|
|
e1403bfbeb | ||
|
|
c38dd2d072 | ||
|
|
f9d9e74d6b | ||
|
|
2ab597addc | ||
|
|
33e394d5ed | ||
|
|
4577de3b17 | ||
|
|
658b529840 | ||
|
|
4aed769da4 | ||
|
|
6c753290aa | ||
|
|
59c91607db | ||
|
|
b0530d59af | ||
|
|
b1ff829745 | ||
|
|
d202b536b5 | ||
|
|
2ac5ef10ba | ||
|
|
8a2ab68e9a | ||
|
|
877220caa0 | ||
|
|
0686c08de3 | ||
|
|
d3f1f8b906 | ||
|
|
33a69d3ef6 | ||
|
|
d91b451d7b | ||
|
|
9e17149e5e | ||
|
|
de61b032b7 | ||
|
|
bdbf9bd04d | ||
|
|
ccd589ef38 | ||
|
|
b4c3753566 | ||
|
|
2fdadb98ac | ||
|
|
a33847a878 | ||
|
|
88cf535c8e | ||
|
|
ea0b59d7c0 | ||
|
|
39b93cdb5b | ||
|
|
e27fc05d5f | ||
|
|
20c010fef1 | ||
|
|
1a7911e9e2 | ||
|
|
cb09b5bc5f | ||
|
|
a99031bdd2 | ||
|
|
ada8a98362 | ||
|
|
e176e81bae | ||
|
|
b64dd66c2c | ||
|
|
7fd162601b | ||
|
|
c1f346c935 | ||
|
|
36d47bd2d8 | ||
|
|
26cdb6521f | ||
|
|
014f1986de | ||
|
|
3ef328421f | ||
|
|
c4fe69d826 | ||
|
|
6fd64b2209 | ||
|
|
585e36793a | ||
|
|
09b9370e13 | ||
|
|
a76a4996b4 | ||
|
|
b28d60b6dc | ||
|
|
a716eda14c | ||
|
|
99f1524183 | ||
|
|
7ce6454915 | ||
|
|
93cb21f55a | ||
|
|
0aca769d06 | ||
|
|
c0bda6257e | ||
|
|
ae22246f05 | ||
|
|
e2931d9fa2 | ||
|
|
e6a8a31b53 | ||
|
|
ee86c751f7 | ||
|
|
073b212c05 | ||
|
|
cd8e2124c4 | ||
|
|
9dd36bd20e | ||
|
|
b1404356ff | ||
|
|
f467bb7957 | ||
|
|
597b14cf4c | ||
|
|
2bdd6df755 | ||
|
|
ad958f10bd | ||
|
|
e5eb7c3906 | ||
|
|
667d42d5da | ||
|
|
a23a9ad15a | ||
|
|
4482ffc66b | ||
|
|
493efabd2e | ||
|
|
a0355e276d | ||
|
|
b5c993e153 | ||
|
|
da3ee3f1fe | ||
|
|
d9fc595444 | ||
|
|
06d13e0f6e | ||
|
|
7dc3266133 | ||
|
|
2c96444058 | ||
|
|
39c48e7f79 | ||
|
|
b6e1989f17 | ||
|
|
45b5b2a376 | ||
|
|
05726ec94c | ||
|
|
6340ff43d8 | ||
|
|
e64b26c7e1 | ||
|
|
c829a7de82 | ||
|
|
0205bfb066 | ||
|
|
5fd7d63388 | ||
|
|
e1ea2cb532 | ||
|
|
88d151aa35 | ||
|
|
4e7c93a322 | ||
|
|
281c7628c1 | ||
|
|
1b24bc14cd | ||
|
|
be3d2fca05 | ||
|
|
327e11e7c7 | ||
|
|
bd999345f4 | ||
|
|
081776b4f9 | ||
|
|
8c328ce5d7 |
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||||
|
"changelog": "@changesets/cli/changelog",
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "public",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": []
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker
|
|
||||||
{
|
|
||||||
"name": "Docker outside of Docker",
|
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
|
||||||
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
|
|
||||||
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/docker-from-docker:1": {
|
|
||||||
"version": "latest",
|
|
||||||
"enableNonRootDocker": "true",
|
|
||||||
"moby": "true"
|
|
||||||
},
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/npm-package:1": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/jest:2": {},
|
|
||||||
"ghcr.io/devcontainers-contrib/features/prisma:2": {},
|
|
||||||
"ghcr.io/guiyomh/features/vim:0": {}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use this environment variable if you need to bind mount your local source code into a new container.
|
|
||||||
"remoteEnv": {
|
|
||||||
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
|
|
||||||
},
|
|
||||||
|
|
||||||
"hostRequirements": {
|
|
||||||
"cpus": 4,
|
|
||||||
"memory": "8gb"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "./deploy/install.sh"
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
// "customizations": {},
|
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
|
||||||
// "remoteUser": "root"
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
# - WEB3
|
# - WEB3
|
||||||
# - SALESFORCE
|
# - SALESFORCE
|
||||||
# - ZOHOCRM
|
# - ZOHOCRM
|
||||||
|
# - ZOHO_BIGIN
|
||||||
|
|
||||||
# - APP STORE **********************************************************************************************
|
# - APP STORE **********************************************************************************************
|
||||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||||
|
|||||||
+11
-7
@@ -79,6 +79,9 @@ CALENDSO_ENCRYPTION_KEY=
|
|||||||
# Intercom Config
|
# Intercom Config
|
||||||
NEXT_PUBLIC_INTERCOM_APP_ID=
|
NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||||
|
|
||||||
|
# Secret to enable Intercom Identity Verification
|
||||||
|
INTERCOM_SECRET=
|
||||||
|
|
||||||
# Zendesk Config
|
# Zendesk Config
|
||||||
NEXT_PUBLIC_ZENDESK_KEY=
|
NEXT_PUBLIC_ZENDESK_KEY=
|
||||||
|
|
||||||
@@ -135,12 +138,16 @@ API_KEY_PREFIX=cal_
|
|||||||
EMAIL_FROM='notifications@yourselfhostedcal.com'
|
EMAIL_FROM='notifications@yourselfhostedcal.com'
|
||||||
|
|
||||||
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
|
# Configure SMTP settings (@see https://nodemailer.com/smtp/).
|
||||||
|
# Configuration to receive emails locally (mailhog)
|
||||||
|
EMAIL_SERVER_HOST='localhost'
|
||||||
|
EMAIL_SERVER_PORT=1025
|
||||||
|
|
||||||
# Note: The below configuration for Office 365 has been verified to work.
|
# Note: The below configuration for Office 365 has been verified to work.
|
||||||
EMAIL_SERVER_HOST='smtp.office365.com'
|
# EMAIL_SERVER_HOST='smtp.office365.com'
|
||||||
EMAIL_SERVER_PORT=587
|
# EMAIL_SERVER_PORT=587
|
||||||
EMAIL_SERVER_USER='<office365_emailAddress>'
|
# EMAIL_SERVER_USER='<office365_emailAddress>'
|
||||||
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
|
# Keep in mind that if you have 2FA enabled, you will need to provision an App Password.
|
||||||
EMAIL_SERVER_PASSWORD='<office365_password>'
|
# EMAIL_SERVER_PASSWORD='<office365_password>'
|
||||||
|
|
||||||
# The following configuration for Gmail has been verified to work.
|
# The following configuration for Gmail has been verified to work.
|
||||||
# EMAIL_SERVER_HOST='smtp.gmail.com'
|
# EMAIL_SERVER_HOST='smtp.gmail.com'
|
||||||
@@ -160,9 +167,6 @@ CLOSECOM_API_KEY=
|
|||||||
# Sendgrid internal sync service
|
# Sendgrid internal sync service
|
||||||
SENDGRID_SYNC_API_KEY=
|
SENDGRID_SYNC_API_KEY=
|
||||||
|
|
||||||
# Sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
|
||||||
|
|
||||||
# Change your Brand
|
# Change your Brand
|
||||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Cron - mark stale for inactive issues
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
@@ -20,4 +21,4 @@ jobs:
|
|||||||
days-before-pr-stale: 14
|
days-before-pr-stale: 14
|
||||||
days-before-pr-close: 7
|
days-before-pr-close: 7
|
||||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued.
|
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Analyze bundle
|
- name: Analyze bundle
|
||||||
run: |
|
run: |
|
||||||
cd apps/web
|
cd apps/web
|
||||||
npx -p nextjs-bundle-analysis report
|
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||||
|
|
||||||
- name: Create Comment
|
- name: Create Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ name: PR Update
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [ready_for_review, review_requested]
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**.md"
|
- "**.md"
|
||||||
- ".github/CODEOWNERS"
|
- ".github/CODEOWNERS"
|
||||||
|
merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
const https = require("https");
|
const https = require("https");
|
||||||
|
|
||||||
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
||||||
|
|
||||||
// Get the labels from issues linked to the PR
|
// Get the labels from issues linked to the PR
|
||||||
const query = `
|
const query = `
|
||||||
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
@@ -38,7 +37,7 @@ async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Content-Length": graphqlData.length,
|
"Content-Length": graphqlData.length,
|
||||||
"Authorization": "Bearer " + token,
|
Authorization: "Bearer " + token,
|
||||||
"User-Agent": "Node.js",
|
"User-Agent": "Node.js",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -52,14 +51,7 @@ async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|||||||
responseBody += chunk;
|
responseBody += chunk;
|
||||||
});
|
});
|
||||||
response.on("end", () => {
|
response.on("end", () => {
|
||||||
resolve(
|
resolve(JSON.parse(responseBody)?.data?.repository?.pullRequest?.closingIssuesReferences?.nodes);
|
||||||
JSON.parse(responseBody)
|
|
||||||
?.data
|
|
||||||
?.repository
|
|
||||||
?.pullRequest
|
|
||||||
?.closingIssuesReferences
|
|
||||||
?.nodes
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,7 +88,7 @@ async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Content-Length": labelsData.length,
|
"Content-Length": labelsData.length,
|
||||||
"Authorization": "Bearer " + token,
|
Authorization: "Bearer " + token,
|
||||||
"User-Agent": "Node.js",
|
"User-Agent": "Node.js",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -128,9 +120,7 @@ async function applyLabelFromLinkedIssueToPR(pr, token) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Applied labels: ${labels.join(", ")} to PR #${
|
`Applied labels: ${labels.join(", ")} to PR #${pr.number} from linked issue #${issue.number}`
|
||||||
pr.number
|
|
||||||
} from linked issue #${issue.number}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
name: TODO to Issue
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
MANUAL_COMMIT_REF:
|
|
||||||
description: "The SHA of the commit to get the diff for"
|
|
||||||
required: true
|
|
||||||
MANUAL_BASE_REF:
|
|
||||||
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
|
|
||||||
required: false
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
todos:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: "Add TODOs to issues"
|
|
||||||
uses: "alstr/todo-to-issue-action@v4"
|
|
||||||
env:
|
|
||||||
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
|
|
||||||
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
|
|
||||||
with:
|
|
||||||
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
|
|
||||||
+2
-1
@@ -65,8 +65,9 @@ tsconfig.tsbuildinfo
|
|||||||
# turbo
|
# turbo
|
||||||
.turbo
|
.turbo
|
||||||
|
|
||||||
# Prisma-Zod
|
# Prisma generated files
|
||||||
packages/prisma/zod/*.ts
|
packages/prisma/zod/*.ts
|
||||||
|
packages/prisma/enums
|
||||||
|
|
||||||
# Builds
|
# Builds
|
||||||
dist
|
dist
|
||||||
|
|||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
tasks:
|
||||||
|
- init: |
|
||||||
|
yarn &&
|
||||||
|
cp .env.example .env &&
|
||||||
|
next_auth_secret=$(openssl rand -base64 32) &&
|
||||||
|
calendso_encryption_key=$(openssl rand -base64 24) &&
|
||||||
|
sed -i -e "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$next_auth_secret|" \
|
||||||
|
-e "s|^CALENDSO_ENCRYPTION_KEY=.*|CALENDSO_ENCRYPTION_KEY=$calendso_encryption_key|" .env
|
||||||
|
command: yarn dx
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
visibility: public
|
||||||
|
onOpen: open-preview
|
||||||
|
- port: 5420
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 1025
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
- port: 8025
|
||||||
|
visibility: private
|
||||||
|
onOpen: ignore
|
||||||
|
|
||||||
|
github:
|
||||||
|
prebuilds:
|
||||||
|
master: true
|
||||||
|
pullRequests: true
|
||||||
|
pullRequestsFromForks: true
|
||||||
|
addCheck: true
|
||||||
|
addComment: true
|
||||||
|
addBadge: true
|
||||||
|
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- DavidAnson.vscode-markdownlint
|
||||||
|
- yzhang.markdown-all-in-one
|
||||||
|
- esbenp.prettier-vscode
|
||||||
|
- dbaeumer.vscode-eslint
|
||||||
|
- bradlc.vscode-tailwindcss
|
||||||
|
- ban.spellright
|
||||||
|
- stripe.vscode-stripe
|
||||||
|
- Prisma.prisma
|
||||||
@@ -14,4 +14,5 @@ public
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintignore
|
.eslintignore
|
||||||
packages/prisma/zod
|
packages/prisma/zod
|
||||||
|
packages/prisma/enums
|
||||||
apps/web/public/embed
|
apps/web/public/embed
|
||||||
Vendored
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
},
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"spellright.language": ["en"],
|
"spellright.language": ["en"],
|
||||||
"spellright.documentTypes": ["markdown", "typescript"],
|
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
//prettier-ignore
|
||||||
|
module.exports = {
|
||||||
|
name: "@yarnpkg/plugin-workspace-tools",
|
||||||
|
factory: function (require) {
|
||||||
|
var plugin=(()=>{var yr=Object.create;var we=Object.defineProperty;var _r=Object.getOwnPropertyDescriptor;var Er=Object.getOwnPropertyNames;var br=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty;var W=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof require<"u"?require:r)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});var q=(e,r)=>()=>(r||e((r={exports:{}}).exports,r),r.exports),Cr=(e,r)=>{for(var t in r)we(e,t,{get:r[t],enumerable:!0})},Je=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let s of Er(r))!xr.call(e,s)&&s!==t&&we(e,s,{get:()=>r[s],enumerable:!(n=_r(r,s))||n.enumerable});return e};var Be=(e,r,t)=>(t=e!=null?yr(br(e)):{},Je(r||!e||!e.__esModule?we(t,"default",{value:e,enumerable:!0}):t,e)),wr=e=>Je(we({},"__esModule",{value:!0}),e);var ve=q(ee=>{"use strict";ee.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;ee.find=(e,r)=>e.nodes.find(t=>t.type===r);ee.exceedsLimit=(e,r,t=1,n)=>n===!1||!ee.isInteger(e)||!ee.isInteger(r)?!1:(Number(r)-Number(e))/Number(t)>=n;ee.escapeNode=(e,r=0,t)=>{let n=e.nodes[r];!n||(t&&n.type===t||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};ee.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0===0?(e.invalid=!0,!0):!1;ee.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0===0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;ee.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;ee.reduce=e=>e.reduce((r,t)=>(t.type==="text"&&r.push(t.value),t.type==="range"&&(t.type="text"),r),[]);ee.flatten=(...e)=>{let r=[],t=n=>{for(let s=0;s<n.length;s++){let i=n[s];Array.isArray(i)?t(i,r):i!==void 0&&r.push(i)}return r};return t(e),r}});var He=q((zn,rt)=>{"use strict";var tt=ve();rt.exports=(e,r={})=>{let t=(n,s={})=>{let i=r.escapeInvalid&&tt.isInvalidBrace(s),a=n.invalid===!0&&r.escapeInvalid===!0,c="";if(n.value)return(i||a)&&tt.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let p of n.nodes)c+=t(p);return c};return t(e)}});var st=q((Vn,nt)=>{"use strict";nt.exports=function(e){return typeof e=="number"?e-e===0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var ht=q((Jn,pt)=>{"use strict";var at=st(),le=(e,r,t)=>{if(at(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(r===void 0||e===r)return String(e);if(at(r)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n={relaxZeros:!0,...t};typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),i=String(n.shorthand),a=String(n.capture),c=String(n.wrap),p=e+":"+r+"="+s+i+a+c;if(le.cache.hasOwnProperty(p))return le.cache[p].result;let m=Math.min(e,r),h=Math.max(e,r);if(Math.abs(m-h)===1){let y=e+"|"+r;return n.capture?`(${y})`:n.wrap===!1?y:`(?:${y})`}let R=ft(e)||ft(r),f={min:e,max:r,a:m,b:h},$=[],_=[];if(R&&(f.isPadded=R,f.maxLen=String(f.max).length),m<0){let y=h<0?Math.abs(h):1;_=it(y,Math.abs(m),f,n),m=f.a=0}return h>=0&&($=it(m,h,f,n)),f.negatives=_,f.positives=$,f.result=Sr(_,$,n),n.capture===!0?f.result=`(${f.result})`:n.wrap!==!1&&$.length+_.length>1&&(f.result=`(?:${f.result})`),le.cache[p]=f,f.result};function Sr(e,r,t){let n=Pe(e,r,"-",!1,t)||[],s=Pe(r,e,"",!1,t)||[],i=Pe(e,r,"-?",!0,t)||[];return n.concat(i).concat(s).join("|")}function vr(e,r){let t=1,n=1,s=ut(e,t),i=new Set([r]);for(;e<=s&&s<=r;)i.add(s),t+=1,s=ut(e,t);for(s=ct(r+1,n)-1;e<s&&s<=r;)i.add(s),n+=1,s=ct(r+1,n)-1;return i=[...i],i.sort(Tr),i}function Hr(e,r,t){if(e===r)return{pattern:e,count:[],digits:0};let n=$r(e,r),s=n.length,i="",a=0;for(let c=0;c<s;c++){let[p,m]=n[c];p===m?i+=p:p!=="0"||m!=="9"?i+=kr(p,m,t):a++}return a&&(i+=t.shorthand===!0?"\\d":"[0-9]"),{pattern:i,count:[a],digits:s}}function it(e,r,t,n){let s=vr(e,r),i=[],a=e,c;for(let p=0;p<s.length;p++){let m=s[p],h=Hr(String(a),String(m),n),R="";if(!t.isPadded&&c&&c.pattern===h.pattern){c.count.length>1&&c.count.pop(),c.count.push(h.count[0]),c.string=c.pattern+lt(c.count),a=m+1;continue}t.isPadded&&(R=Lr(m,t,n)),h.string=R+h.pattern+lt(h.count),i.push(h),a=m+1,c=h}return i}function Pe(e,r,t,n,s){let i=[];for(let a of e){let{string:c}=a;!n&&!ot(r,"string",c)&&i.push(t+c),n&&ot(r,"string",c)&&i.push(t+c)}return i}function $r(e,r){let t=[];for(let n=0;n<e.length;n++)t.push([e[n],r[n]]);return t}function Tr(e,r){return e>r?1:r>e?-1:0}function ot(e,r,t){return e.some(n=>n[r]===t)}function ut(e,r){return Number(String(e).slice(0,-r)+"9".repeat(r))}function ct(e,r){return e-e%Math.pow(10,r)}function lt(e){let[r=0,t=""]=e;return t||r>1?`{${r+(t?","+t:"")}}`:""}function kr(e,r,t){return`[${e}${r-e===1?"":"-"}${r}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Lr(e,r,t){if(!r.isPaddLine truncated
|
||||||
|
`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Nt=q((ss,Ot)=>{"use strict";var jr=He(),{MAX_LENGTH:Tt,CHAR_BACKSLASH:Ge,CHAR_BACKTICK:Fr,CHAR_COMMA:Qr,CHAR_DOT:Xr,CHAR_LEFT_PARENTHESES:Zr,CHAR_RIGHT_PARENTHESES:Yr,CHAR_LEFT_CURLY_BRACE:zr,CHAR_RIGHT_CURLY_BRACE:Vr,CHAR_LEFT_SQUARE_BRACKET:kt,CHAR_RIGHT_SQUARE_BRACKET:Lt,CHAR_DOUBLE_QUOTE:Jr,CHAR_SINGLE_QUOTE:en,CHAR_NO_BREAK_SPACE:tn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:rn}=$t(),nn=(e,r={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let t=r||{},n=typeof t.maxLength=="number"?Math.min(Tt,t.maxLength):Tt;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},i=[s],a=s,c=s,p=0,m=e.length,h=0,R=0,f,$={},_=()=>e[h++],y=E=>{if(E.type==="text"&&c.type==="dot"&&(c.type="text"),c&&c.type==="text"&&E.type==="text"){c.value+=E.value;return}return a.nodes.push(E),E.parent=a,E.prev=c,c=E,E};for(y({type:"bos"});h<m;)if(a=i[i.length-1],f=_(),!(f===rn||f===tn)){if(f===Ge){y({type:"text",value:(r.keepEscaping?f:"")+_()});continue}if(f===Lt){y({type:"text",value:"\\"+f});continue}if(f===kt){p++;let E=!0,S;for(;h<m&&(S=_());){if(f+=S,S===kt){p++;continue}if(S===Ge){f+=_();continue}if(S===Lt&&(p--,p===0))break}y({type:"text",value:f});continue}if(f===Zr){a=y({type:"paren",nodes:[]}),i.push(a),y({type:"text",value:f});continue}if(f===Yr){if(a.type!=="paren"){y({type:"text",value:f});continue}a=i.pop(),y({type:"text",value:f}),a=i[i.length-1];continue}if(f===Jr||f===en||f===Fr){let E=f,S;for(r.keepQuotes!==!0&&(f="");h<m&&(S=_());){if(S===Ge){f+=S+_();continue}if(S===E){r.keepQuotes===!0&&(f+=S);break}f+=S}y({type:"text",value:f});continue}if(f===zr){R++;let E=c.value&&c.value.slice(-1)==="$"||a.dollar===!0;a=y({type:"brace",open:!0,close:!1,dollar:E,depth:R,commas:0,ranges:0,nodes:[]}),i.push(a),y({type:"open",value:f});continue}if(f===Vr){if(a.type!=="brace"){y({type:"text",value:f});continue}let E="close";a=i.pop(),a.close=!0,y({type:E,value:f}),R--,a=i[i.length-1];continue}if(f===Qr&&R>0){if(a.ranges>0){a.ranges=0;let E=a.nodes.shift();a.nodes=[E,{type:"text",value:jr(a)}]}y({type:"comma",value:f}),a.commas++;continue}if(f===Xr&&R>0&&a.commas===0){let E=a.nodes;if(R===0||E.length===0){y({type:"text",value:f});continue}if(c.type==="dot"){if(a.range=[],c.value+=f,c.type="range",a.nodes.length!==3&&a.nodes.length!==5){a.invalid=!0,a.ranges=0,c.type="text";continue}a.ranges++,a.args=[];continue}if(c.type==="range"){E.pop();let S=E[E.length-1];S.value+=c.value+f,c=S,a.ranges--;continue}y({type:"dot",value:f});continue}y({type:"text",value:f})}do if(a=i.pop(),a.type!=="root"){a.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let E=i[i.length-1],S=E.nodes.indexOf(a);E.nodes.splice(S,1,...a.nodes)}while(i.length>0);return y({type:"eos"}),s};Ot.exports=nn});var Pt=q((as,Bt)=>{"use strict";var It=He(),sn=Ct(),an=vt(),on=Nt(),Z=(e,r={})=>{let t=[];if(Array.isArray(e))for(let n of e){let s=Z.create(n,r);Array.isArray(s)?t.push(...s):t.push(s)}else t=[].concat(Z.create(e,r));return r&&r.expand===!0&&r.nodupes===!0&&(t=[...new Set(t)]),t};Z.parse=(e,r={})=>on(e,r);Z.stringify=(e,r={})=>It(typeof e=="string"?Z.parse(e,r):e,r);Z.compile=(e,r={})=>(typeof e=="string"&&(e=Z.parse(e,r)),sn(e,r));Z.expand=(e,r={})=>{typeof e=="string"&&(e=Z.parse(e,r));let t=an(e,r);return r.noempty===!0&&(t=t.filter(Boolean)),r.nodupes===!0&&(t=[...new Set(t)]),t};Z.create=(e,r={})=>e===""||e.length<3?[e]:r.expand!==!0?Z.compile(e,r):Z.expand(e,r);Bt.exports=Z});var me=q((is,qt)=>{"use strict";var un=W("path"),se="\\\\/",Mt=`[^${se}]`,ie="\\.",cn="\\+",ln="\\?",Te="\\/",fn="(?=.)",Dt="[^/]",qe=`(?:${Te}|$)`,Ut=`(?:^|${Te})`,Ke=`${ie}{1,2}${qe}`,pn=`(?!${ie})`,hn=`(?!${Ut}${Ke})`,dn=`(?!${ie}{0,1}${qe})`,gn=`(?!${Ke})`,An=`[^.${Te}]`,mn=`${Dt}*?`,Gt={DOT_LITERAL:ie,PLUS_LITERAL:cn,QMARK_LITERAL:ln,SLASH_LITERAL:Te,ONE_CHAR:fn,QMARK:Dt,END_ANCHOR:qe,DOTS_SLASH:Ke,NO_DOT:pn,NO_DOTS:hn,NO_DOT_SLASH:dn,NO_DOTS_SLASH:gn,QMARK_NO_DOT:An,STAR:mn,START_ANCHOR:Ut},Rn={...Gt,SLASH_LITERAL:`[${se}]`,QMARK:Mt,STAR:`${Mt}*?`,DOTS_SLASH:`${ie}{1,2}(?:[${se}]|$)`,NO_DOT:`(?!${ie})`,NO_DOTS:`(?!(?:^|[${se}])${ie}{1,2}(?:[${se}]|$))`,NO_DOT_SLASH:`(?!${ie}{0,1}(?:[${se}]|$))`,NO_DOTS_SLASH:`(?!${ie}{1,2}(?:[${se}]|$))`,QMARK_NO_DOT:`[^.${se}]`,START_ANCHOR:`(?:^|[${se}])`,END_ANCHOR:`(?:[${se}]|$)`},yn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Line truncated
|
||||||
|
/*!
|
||||||
|
* fill-range <https://github.com/jonschlinkert/fill-range>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Jon Schlinkert.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
/*!
|
||||||
|
* is-number <https://github.com/jonschlinkert/is-number>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
/*!
|
||||||
|
* to-regex-range <https://github.com/micromatch/to-regex-range>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2015-present, Jon Schlinkert.
|
||||||
|
* Released under the MIT License.
|
||||||
|
*/
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,5 +3,7 @@ nodeLinker: node-modules
|
|||||||
plugins:
|
plugins:
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||||
spec: "@yarnpkg/plugin-interactive-tools"
|
spec: "@yarnpkg/plugin-interactive-tools"
|
||||||
|
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||||
|
spec: "@yarnpkg/plugin-workspace-tools"
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||||
+6
-1
@@ -86,7 +86,12 @@ To develop locally:
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Start developing and watch for code changes:
|
5. Set up your `.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.
|
||||||
|
|
||||||
|
6. Start developing and watch for code changes:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn dev
|
yarn dev
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
|||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<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://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://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://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>
|
<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://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://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://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>
|
<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>
|
</p>
|
||||||
|
|
||||||
<!-- ABOUT THE PROJECT -->
|
<!-- 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">
|
<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
|
The open source Calendly alternative. You are in charge
|
||||||
of your own data, workflow and appearance.
|
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.
|
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
|
## Recognition
|
||||||
|
|
||||||
#### Hacker News
|
#### [Hacker News](https://news.ycombinator.com/from?site=cal.com)
|
||||||
|
|
||||||
<a href="https://news.ycombinator.com/item?id=34507672">
|
<a href="https://news.ycombinator.com/item?id=34507672">
|
||||||
<img
|
<img
|
||||||
@@ -77,22 +77,22 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
|||||||
/>
|
/>
|
||||||
</a>
|
</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
|
### Built With
|
||||||
|
|
||||||
- [Next.js](https://nextjs.org/?ref=cal.com)
|
- [Next.js](https://nextjs.org/?ref=cal.com)
|
||||||
- [tRPC](https://trpc.io/?ref=cal.com)
|
- [tRPC](https://trpc.io/?ref=cal.com)
|
||||||
- [React](https://reactjs.org/?ref=cal.com)
|
- [React.js](https://reactjs.org/?ref=cal.com)
|
||||||
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
- [Tailwind CSS](https://tailwindcss.com/?ref=cal.com)
|
||||||
- [Prisma](https://prisma.io/?ref=cal.com)
|
- [Prisma.io](https://prisma.io/?ref=cal.com)
|
||||||
- [Daily.co](https://go.cal.com/daily)
|
- [Daily.co](https://go.cal.com/daily)
|
||||||
|
|
||||||
## Stay Up-to-Date
|
## 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:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ To get a local copy up and running, please follow these simple steps.
|
|||||||
|
|
||||||
### Prerequisites
|
### 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)
|
- Node.js (Version: >=15.x <17)
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
@@ -121,6 +121,9 @@ Here is what you need to be able to run Cal.
|
|||||||
```sh
|
```sh
|
||||||
git clone https://github.com/calcom/cal.com.git
|
git clone https://github.com/calcom/cal.com.git
|
||||||
```
|
```
|
||||||
|
> If you are on windows, run the following command on `gitbash` with admin privileges: <br>
|
||||||
|
```git clone -c core.symlinks=true https://github.com/calcom/cal.com.git``` <br>
|
||||||
|
See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
||||||
|
|
||||||
1. Go to the project folder
|
1. Go to the project folder
|
||||||
|
|
||||||
@@ -134,10 +137,10 @@ Here is what you need to be able to run Cal.
|
|||||||
yarn
|
yarn
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Set up your .env file
|
1. Set up your `.env` file
|
||||||
- Duplicate `.env.example` to `.env`
|
- Duplicate `.env.example` to `.env`
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the .env file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the .env file.
|
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
#### Quick start with `yarn dx`
|
#### Quick start with `yarn dx`
|
||||||
|
|
||||||
@@ -150,53 +153,63 @@ yarn dx
|
|||||||
|
|
||||||
#### Development tip
|
#### 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
|
```sh
|
||||||
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||||
```
|
```
|
||||||
|
#### Gitpod Setup
|
||||||
|
|
||||||
|
1. Click the button below to open this project in Gitpod.
|
||||||
|
|
||||||
|
2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed.
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Manual setup
|
#### 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>'
|
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||||
```
|
```
|
||||||
|
|
||||||
<details>
|
<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 local DB</summary>
|
||||||
|
|
||||||
1. Create a free account with [Heroku](https://www.heroku.com/).
|
1. [Download](https://www.postgresql.org/download/) and install postgres in your local (if you don't have it already).
|
||||||
|
|
||||||
2. Create a new app.
|
2. Create your own local db by executing `createDB <DB name>`
|
||||||
<img width="306" alt="Create an App" src="https://user-images.githubusercontent.com/16905768/115322780-b3d58c00-a17e-11eb-8a52-b758fb0ea942.png">
|
|
||||||
|
|
||||||
3. In your new app, go to `Overview` and next to `Installed add-ons`, click `Configure Add-ons`. We need this to set up our database.
|
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
|
||||||

|
|
||||||
|
|
||||||
4. Once you clicked on `Configure Add-ons`, click on `Find more add-ons` and search for `postgres`. One of the options will be `Heroku Postgres` - click on that option.
|
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||||

|

|
||||||
|
|
||||||
5. Once the pop-up appears, click `Submit Order Form` - plan name should be `Hobby Dev - Free`.
|
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||||
<img width="512" alt="Submit Order Form" src="https://user-images.githubusercontent.com/16905768/115323265-b4baed80-a17f-11eb-99f0-d67f019aa6df.png">
|
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||||
|
|
||||||
6. Once you completed the above steps, click on your newly created `Heroku Postgres` and go to its `Settings`.
|
|
||||||

|
|
||||||
|
|
||||||
7. In `Settings`, copy your URI to your Cal.com .env file and replace the `postgresql://<user>:<pass>@<db-host>:<db-port>` with it.
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
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>
|
</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).
|
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||||
|
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||||
|
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||||
|
|
||||||
|
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
|
||||||
|
|
||||||
|
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`)
|
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
yarn workspace @calcom/prisma db-deploy
|
yarn workspace @calcom/prisma db-deploy
|
||||||
```
|
```
|
||||||
|
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker pull mailhog/mailhog
|
||||||
|
docker run -d -p 8025:8025 -p 1025:1025 mailhog/mailhog
|
||||||
|
```
|
||||||
|
|
||||||
1. Run (in development mode)
|
1. Run (in development mode)
|
||||||
|
|
||||||
@@ -206,7 +219,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
|||||||
|
|
||||||
#### Setting up your first user
|
#### 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
|
```sh
|
||||||
yarn db-studio
|
yarn db-studio
|
||||||
@@ -251,7 +264,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
|
|||||||
yarn workspace @calcom/prisma db-migrate
|
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:
|
In a production environment, run:
|
||||||
|
|
||||||
@@ -285,7 +298,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
|
|||||||
|
|
||||||
### Docker
|
### 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).
|
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
||||||
|
|
||||||
@@ -298,14 +311,14 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
|
|||||||
### Heroku
|
### Heroku
|
||||||
|
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
<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>
|
</a>
|
||||||
|
|
||||||
### Railway
|
### Railway
|
||||||
|
|
||||||
[](https://railway.app/new/template/cal)
|
[](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
|
### Vercel
|
||||||
|
|
||||||
@@ -386,7 +399,7 @@ Don't code but still want to contribute? Join our [slack](https://cal.com/slack)
|
|||||||
8. Select Web Application as the Application Type.
|
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.
|
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.
|
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_
|
#### _Adding google calendar to Cal.com App Store_
|
||||||
|
|
||||||
@@ -423,7 +436,7 @@ following
|
|||||||
5. Choose "User-managed app" as the app type.
|
5. Choose "User-managed app" as the app type.
|
||||||
6. De-select the option to publish the app on the Zoom App Marketplace.
|
6. De-select the option to publish the app on the Zoom App Marketplace.
|
||||||
7. Click "Create".
|
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.
|
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.
|
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`.
|
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 +448,8 @@ following
|
|||||||
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
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.
|
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||||
3. Copy your API key.
|
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.
|
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.
|
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
|
### Obtaining HubSpot Client ID and Secret
|
||||||
|
|
||||||
@@ -445,12 +458,16 @@ following
|
|||||||
3. Click "Create app" button top right.
|
3. Click "Create app" button top right.
|
||||||
4. Fill in any information you want in the "App info" tab
|
4. Fill in any information you want in the "App info" tab
|
||||||
5. Go to tab "Auth"
|
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.
|
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`
|
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.
|
9. Click the "Save" button at the bottom footer.
|
||||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||||
|
|
||||||
|
### Obtaining Webex Client ID and Secret
|
||||||
|
|
||||||
|
[See Webex Readme](./packages/app-store/webex/)
|
||||||
|
|
||||||
### Obtaining ZohoCRM Client ID and Secret
|
### Obtaining ZohoCRM Client ID and Secret
|
||||||
|
|
||||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||||
@@ -458,31 +475,33 @@ following
|
|||||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||||
4. Fill in any information you want in the "Client Details" tab
|
4. Fill in any information you want in the "Client Details" tab
|
||||||
5. Go to tab "Client Secret" 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.
|
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.
|
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.
|
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.
|
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
|
## Workflows
|
||||||
|
|
||||||
### Setting up SendGrid for Email reminders
|
### Setting up SendGrid for Email reminders
|
||||||
|
|
||||||
1. Create a SendGrid account (https://signup.sendgrid.com/)
|
1. Create a SendGrid account (https://signup.sendgrid.com/)
|
||||||
2. Go to Settings -> API keys and create an API key
|
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
|
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
|
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)
|
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
|
### 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’
|
2. Click ‘Get a Twilio phone number’
|
||||||
3. Copy Account SID to your .env file into the TWILIO_SID field
|
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
|
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
|
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)
|
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)
|
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||||
8. Choose any name for the messaging service
|
8. Choose any name for the messaging service
|
||||||
9. Click 'Add Senders'
|
9. Click 'Add Senders'
|
||||||
@@ -490,15 +509,15 @@ following
|
|||||||
11. Add the listed phone number
|
11. Add the listed phone number
|
||||||
12. Leave all other fields as they are
|
12. Leave all other fields as they are
|
||||||
13. Complete setup and click ‘View my new Messaging Service’
|
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
|
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 -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See `LICENSE` for more information.
|
Distributed under the [AGPLv3 License](https://github.com/calcom/cal.com/blob/main/LICENSE). See `LICENSE` for more information.
|
||||||
|
|
||||||
<!-- ACKNOWLEDGEMENTS -->
|
<!-- ACKNOWLEDGEMENTS -->
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/docs",
|
"name": "@calcom/docs",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
stories: [
|
stories: [
|
||||||
"../intro.stories.mdx",
|
"../intro.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.mdx",
|
"../../../packages/ui/components/**/*.stories.mdx",
|
||||||
|
"../../../packages/atoms/**/*.stories.mdx",
|
||||||
"../../../packages/features/**/*.stories.mdx",
|
"../../../packages/features/**/*.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
@@ -70,4 +71,5 @@ module.exports = {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
typescript: { reactDocgen: "react-docgen" },
|
||||||
};
|
};
|
||||||
@@ -68,5 +68,3 @@ public/embed
|
|||||||
|
|
||||||
# Copied app-store images
|
# Copied app-store images
|
||||||
public/app-store
|
public/app-store
|
||||||
# Sentry
|
|
||||||
.sentryclirc
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# @calcom/web
|
||||||
|
|
||||||
|
## 2.7.16
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies
|
||||||
|
- @calcom/embed-snippet@1.0.7
|
||||||
|
- @calcom/embed-react@1.0.12
|
||||||
|
- @calcom/embed-core@1.1.5
|
||||||
+264
-273
@@ -1,5 +1,6 @@
|
|||||||
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
import type { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { MutableRefObject, RefObject } from "react";
|
import type { MutableRefObject, RefObject } from "react";
|
||||||
@@ -21,11 +22,10 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
|
Select,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
|
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 EmbedType = "inline" | "floating-popup" | "element-click";
|
||||||
type EmbedFramework = "react" | "HTML";
|
type EmbedFramework = "react" | "HTML";
|
||||||
|
|
||||||
@@ -305,190 +305,189 @@ const getEmbedTypeSpecificString = ({
|
|||||||
return "";
|
return "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const embeds: {
|
const embeds = (t: TFunction) =>
|
||||||
illustration: React.ReactElement;
|
(() => {
|
||||||
title: string;
|
return [
|
||||||
subtitle: string;
|
{
|
||||||
type: EmbedType;
|
title: t("inline_embed"),
|
||||||
}[] = [
|
subtitle: t("load_inline_content"),
|
||||||
{
|
type: "inline",
|
||||||
title: "Inline Embed",
|
illustration: (
|
||||||
subtitle: "Loads your event type directly inline with your other website content.",
|
<svg
|
||||||
type: "inline",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
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"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<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"
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<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="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="49" 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="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="97" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="73" 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="85" 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="97" 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="109" 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="121" y="99.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="133" y="99.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="85" 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="97" 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="109" 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="121" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="133" y="113.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<path
|
||||||
<rect x="49" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
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"
|
||||||
<rect x="61" y="125.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
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"
|
<rect x="73" y="125.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
fill="#3E3E3E"
|
<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="73" 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="85" 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="97" 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="109" 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="121" y="125.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="133" y="125.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="49" 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="61" 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="73" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="85" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" 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="133" y="137.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="137.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="49" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="137.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="133" y="137.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="49" 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="61" 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="73" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||||
<rect x="85" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="121" 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="133" y="149.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="109" y="149.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="49" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||||
<rect x="121" y="149.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="133" y="149.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="49" 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="61" 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="73" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="109" 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="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
||||||
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
</svg>
|
||||||
<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",
|
||||||
title: "Floating pop-up button",
|
illustration: (
|
||||||
subtitle: "Adds a floating button on your site that launches Cal in a dialog.",
|
<svg
|
||||||
type: "floating-popup",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
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"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<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"
|
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<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="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||||
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
||||||
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||||
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
</svg>
|
||||||
<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",
|
||||||
title: "Pop up via element click",
|
illustration: (
|
||||||
subtitle: "Open your Cal dialog when someone clicks an element.",
|
<svg
|
||||||
type: "element-click",
|
width="100%"
|
||||||
illustration: (
|
height="100%"
|
||||||
<svg
|
className="rounded-md"
|
||||||
width="100%"
|
viewBox="0 0 308 265"
|
||||||
height="100%"
|
fill="none"
|
||||||
className="rounded-md"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
viewBox="0 0 308 265"
|
<path
|
||||||
fill="none"
|
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"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
fill="white"
|
||||||
<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"
|
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||||
fill="white"
|
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
||||||
/>
|
<g filter="url(#filter0_i_3223_14162)">
|
||||||
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<g filter="url(#filter0_i_3223_14162)">
|
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
||||||
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||||
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
</g>
|
||||||
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
|
||||||
</g>
|
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
||||||
<rect x="152" y="48" width="2" height="169" 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 = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
@@ -534,11 +533,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
<!-- Cal ${embedType} embed code ends -->`
|
<!-- Cal ${embedType} embed code ends -->`
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<p className="text-subtle hidden text-sm">
|
<p className="text-subtle hidden text-sm">{t("need_help_embedding")}</p>
|
||||||
{t(
|
|
||||||
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -622,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
|||||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||||
return (
|
return (
|
||||||
<components.Control {...props}>
|
<components.Control {...props}>
|
||||||
<Sun className="text-subtle ml-2 h-4 w-4" />
|
<Sun className="text-subtle mr-2 h-4 w-4" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -642,7 +637,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-start space-y-2 md:flex md:space-y-0">
|
<div className="items-start space-y-2 md:flex md:space-y-0">
|
||||||
{embeds.map((embed, index) => (
|
{embeds(t).map((embed, index) => (
|
||||||
<button
|
<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"
|
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}
|
key={index}
|
||||||
@@ -692,7 +687,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
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 [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||||
@@ -806,11 +801,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
const FloatingPopupPositionOptions = [
|
const FloatingPopupPositionOptions = [
|
||||||
{
|
{
|
||||||
value: "bottom-right",
|
value: "bottom-right",
|
||||||
label: "Bottom Right",
|
label: "Bottom right",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "bottom-left",
|
value: "bottom-left",
|
||||||
label: "Bottom Left",
|
label: "Bottom left",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -834,7 +829,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</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="flex flex-col">
|
||||||
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
@@ -844,7 +839,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
<div className="text-default mb-[9px] text-sm">Window sizing</div>
|
<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]">
|
<div className="mr-[9px]">
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
@@ -892,10 +887,10 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"items-center justify-between",
|
||||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
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 */}
|
{/* Default Values should come from preview iframe */}
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
@@ -910,7 +905,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
defaultValue="Book my Cal"
|
defaultValue={t("book_my_cal")}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 items-center justify-between",
|
"mt-4 items-center justify-between",
|
||||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-2">Position of Button</div>
|
<div className="mb-2">Position of button</div>
|
||||||
<Select
|
<Select
|
||||||
onChange={(position) => {
|
onChange={(position) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -959,54 +954,50 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={FloatingPopupPositionOptions}
|
options={FloatingPopupPositionOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="mt-3 flex flex-col xl:flex-row xl:justify-between">
|
||||||
className={classNames(
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
"mt-4",
|
<div className="whitespace-nowrap">Button color</div>
|
||||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
<div className="mt-2 w-40 xl:mt-0 xl:w-full">
|
||||||
)}>
|
<ColorPicker
|
||||||
<div>Button Color</div>
|
className="w-[130px]"
|
||||||
<div className="w-full">
|
popoverAlign="start"
|
||||||
<ColorPicker
|
container={dialogContentRef?.current ?? undefined}
|
||||||
popoverAlign="start"
|
defaultValue="#000000"
|
||||||
container={dialogContentRef?.current ?? undefined}
|
onChange={(color) => {
|
||||||
defaultValue="#000000"
|
setPreviewState((previewState) => {
|
||||||
onChange={(color) => {
|
return {
|
||||||
setPreviewState((previewState) => {
|
...previewState,
|
||||||
return {
|
floatingPopup: {
|
||||||
...previewState,
|
...previewState.floatingPopup,
|
||||||
floatingPopup: {
|
buttonColor: color,
|
||||||
...previewState.floatingPopup,
|
},
|
||||||
buttonColor: color,
|
};
|
||||||
},
|
});
|
||||||
};
|
}}
|
||||||
});
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||||
<div
|
<div className="whitespace-nowrap">Text color</div>
|
||||||
className={classNames(
|
<div className="mt-2 mb-6 w-40 xl:mt-0 xl:w-full">
|
||||||
"mt-4",
|
<ColorPicker
|
||||||
embedType === "floating-popup" ? "text-emphasis" : "hidden"
|
className="w-[130px]"
|
||||||
)}>
|
popoverAlign="start"
|
||||||
<div>Text Color</div>
|
container={dialogContentRef?.current ?? undefined}
|
||||||
<div className="w-full">
|
defaultValue="#000000"
|
||||||
<ColorPicker
|
onChange={(color) => {
|
||||||
popoverAlign="start"
|
setPreviewState((previewState) => {
|
||||||
container={dialogContentRef?.current ?? undefined}
|
return {
|
||||||
defaultValue="#000000"
|
...previewState,
|
||||||
onChange={(color) => {
|
floatingPopup: {
|
||||||
setPreviewState((previewState) => {
|
...previewState.floatingPopup,
|
||||||
return {
|
buttonTextColor: color,
|
||||||
...previewState,
|
},
|
||||||
floatingPopup: {
|
};
|
||||||
...previewState.floatingPopup,
|
});
|
||||||
buttonTextColor: color,
|
}}
|
||||||
},
|
/>
|
||||||
};
|
</div>
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -1017,7 +1008,30 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleContent>
|
<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">
|
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
<Switch
|
<Switch
|
||||||
checked={previewState.hideEventTypeDetails}
|
checked={previewState.hideEventTypeDetails}
|
||||||
@@ -1056,29 +1070,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</Label>
|
</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>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { isEmpty } from "lodash";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
export function useViewerI18n() {
|
export function useViewerI18n() {
|
||||||
@@ -22,16 +22,18 @@ export function useViewerI18n() {
|
|||||||
*/
|
*/
|
||||||
const I18nLanguageHandler = (): null => {
|
const I18nLanguageHandler = (): null => {
|
||||||
const { i18n } = useTranslation("common");
|
const { i18n } = useTranslation("common");
|
||||||
const locale = useViewerI18n().data?.locale || "en";
|
const locale = useViewerI18n().data?.locale || i18n.language;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (locale && i18n.language && i18n.language !== locale) {
|
// bail early when i18n = {}
|
||||||
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
|
if (isEmpty(i18n)) return;
|
||||||
|
// if locale is ready and the i18n.language does != locale - changeLanguage
|
||||||
|
if (locale && i18n.language !== locale) {
|
||||||
|
i18n.changeLanguage(locale);
|
||||||
}
|
}
|
||||||
|
// set dir="rtl|ltr"
|
||||||
const dir = getDirFromLang(locale);
|
document.dir = i18n.dir();
|
||||||
document.documentElement.setAttribute("lang", locale);
|
document.documentElement.setAttribute("lang", locale);
|
||||||
document.documentElement.setAttribute("dir", dir);
|
|
||||||
}, [locale, i18n]);
|
}, [locale, i18n]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import type { FormEvent } from "react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import Cropper from "react-easy-crop";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui";
|
|
||||||
|
|
||||||
import type { Area } from "@lib/cropImage";
|
|
||||||
import { getCroppedImg } from "@lib/cropImage";
|
|
||||||
import { useFileReader } from "@lib/hooks/useFileReader";
|
|
||||||
|
|
||||||
import Slider from "@components/Slider";
|
|
||||||
|
|
||||||
type ImageUploaderProps = {
|
|
||||||
id: string;
|
|
||||||
buttonMsg: string;
|
|
||||||
handleAvatarChange: (imageSrc: string) => void;
|
|
||||||
imageSrc?: string;
|
|
||||||
target: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FileEvent<T = Element> extends FormEvent<T> {
|
|
||||||
target: EventTarget & T;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is separate to prevent loading the component until file upload
|
|
||||||
function CropContainer({
|
|
||||||
onCropComplete,
|
|
||||||
imageSrc,
|
|
||||||
}: {
|
|
||||||
imageSrc: string;
|
|
||||||
onCropComplete: (croppedAreaPixels: Area) => void;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [crop, setCrop] = useState({ x: 0, y: 0 });
|
|
||||||
const [zoom, setZoom] = useState(1);
|
|
||||||
|
|
||||||
const handleZoomSliderChange = (value: number) => {
|
|
||||||
value < 1 ? setZoom(1) : setZoom(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="crop-container h-40 max-h-40 w-40 rounded-full">
|
|
||||||
<div className="relative h-40 w-40 rounded-full">
|
|
||||||
<Cropper
|
|
||||||
image={imageSrc}
|
|
||||||
crop={crop}
|
|
||||||
zoom={zoom}
|
|
||||||
aspect={1}
|
|
||||||
onCropChange={setCrop}
|
|
||||||
onCropComplete={(croppedArea, croppedAreaPixels) => onCropComplete(croppedAreaPixels)}
|
|
||||||
onZoomChange={setZoom}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
value={zoom}
|
|
||||||
min={1}
|
|
||||||
max={3}
|
|
||||||
step={0.1}
|
|
||||||
label={t("slide_zoom_drag_instructions")}
|
|
||||||
changeHandler={handleZoomSliderChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Use `packages/ui/v2/core/ImageUploader.tsx` */
|
|
||||||
export default function ImageUploader({
|
|
||||||
target,
|
|
||||||
id,
|
|
||||||
buttonMsg,
|
|
||||||
handleAvatarChange,
|
|
||||||
...props
|
|
||||||
}: ImageUploaderProps) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
|
||||||
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
|
|
||||||
|
|
||||||
const [{ result }, setFile] = useFileReader({
|
|
||||||
method: "readAsDataURL",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (props.imageSrc) setImageSrc(props.imageSrc);
|
|
||||||
}, [props.imageSrc]);
|
|
||||||
|
|
||||||
const onInputFile = (e: FileEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.files?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFile(e.target.files[0]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const showCroppedImage = useCallback(
|
|
||||||
async (croppedAreaPixels: Area | null) => {
|
|
||||||
try {
|
|
||||||
if (!croppedAreaPixels) return;
|
|
||||||
const croppedImage = await getCroppedImg(
|
|
||||||
result as string /* result is always string when using readAsDataUrl */,
|
|
||||||
croppedAreaPixels
|
|
||||||
);
|
|
||||||
setImageSrc(croppedImage);
|
|
||||||
handleAvatarChange(croppedImage);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[result, handleAvatarChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
onOpenChange={
|
|
||||||
(opened) => !opened && setFile(null) // unset file on close
|
|
||||||
}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button color="secondary" type="button" className="py-1 text-xs">
|
|
||||||
{buttonMsg}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
|
||||||
<h3 className="font-cal text-emphasis text-lg leading-6" id="modal-title">
|
|
||||||
{t("upload_target", { target })}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="cropper mt-6 flex flex-col items-center justify-center p-8">
|
|
||||||
{!result && (
|
|
||||||
<div className="bg-muted flex h-20 max-h-20 w-20 items-center justify-start rounded-full">
|
|
||||||
{!imageSrc && (
|
|
||||||
<p className="text-inverted w-full text-center text-sm sm:text-xs">
|
|
||||||
{t("no_target", { target })}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{imageSrc && (
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img className="h-20 w-20 rounded-full" src={imageSrc} alt={target} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
|
||||||
<label className="border-default bg-default hover:bg-mutedover:text-emphasis dark:hover:bg-inverted text-default dark:text-inverted focus:ring-empthasis mt-8 rounded-sm border px-3 py-1 text-xs font-medium leading-4 focus:outline-none focus:ring-2 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent">
|
|
||||||
<input
|
|
||||||
onInput={onInputFile}
|
|
||||||
type="file"
|
|
||||||
name={id}
|
|
||||||
placeholder={t("upload_image")}
|
|
||||||
className="pointer-events-none absolute mt-4 opacity-0"
|
|
||||||
accept="image/*"
|
|
||||||
/>
|
|
||||||
{t("choose_a_file")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
|
||||||
<DialogClose onClick={() => showCroppedImage(croppedAreaPixels)}>{t("save")}</DialogClose>
|
|
||||||
<DialogClose color="secondary">{t("cancel")}</DialogClose>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
|
||||||
import { noop } from "lodash";
|
import { noop } from "lodash";
|
||||||
import type { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -6,6 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import type { FC, MouseEventHandler } from "react";
|
import type { FC, MouseEventHandler } from "react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { PermissionContainer } from "@calcom/features/auth/PermissionContainer";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -60,7 +60,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
}
|
}
|
||||||
: noop;
|
: noop;
|
||||||
|
|
||||||
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
const Component = tab.adminRequired ? PermissionContainer : Fragment;
|
||||||
const className = tab.className || "";
|
const className = tab.className || "";
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { DefaultSeo } from "next-seo";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
import AppProviders from "@lib/app-providers";
|
||||||
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
|
export interface CalPageWrapper {
|
||||||
|
(props?: any): JSX.Element;
|
||||||
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
|
const calFont = localFont({
|
||||||
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
|
variable: "--font-cal",
|
||||||
|
preload: true,
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageWrapper(props: AppProps) {
|
||||||
|
const { Component, pageProps, err, router } = props;
|
||||||
|
let pageStatus = "200";
|
||||||
|
|
||||||
|
if (router.pathname === "/404") {
|
||||||
|
pageStatus = "404";
|
||||||
|
} else if (router.pathname === "/500") {
|
||||||
|
pageStatus = "500";
|
||||||
|
}
|
||||||
|
|
||||||
|
// On client side don't let nonce creep into DOM
|
||||||
|
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||||
|
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||||
|
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
||||||
|
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
||||||
|
const providerProps = {
|
||||||
|
...props,
|
||||||
|
pageProps: {
|
||||||
|
...props.pageProps,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Use the layout defined at the page level, if available
|
||||||
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
return (
|
||||||
|
<AppProviders {...providerProps}>
|
||||||
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
nonce={nonce}
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-inter: ${interFont.style.fontFamily};
|
||||||
|
--font-cal: ${calFont.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{getLayout(
|
||||||
|
Component.requiresLicense ? (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
),
|
||||||
|
router
|
||||||
|
)}
|
||||||
|
</AppProviders>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageWrapper;
|
||||||
@@ -46,7 +46,7 @@ export default function SettingsShell({
|
|||||||
...rest
|
...rest
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<div className="sm:mx-auto">
|
<div className="sm:mx-auto">
|
||||||
<NavTabs tabs={tabs} />
|
<NavTabs tabs={tabs} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const Slider = ({
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
changeHandler,
|
|
||||||
...props
|
|
||||||
}: Omit<SliderPrimitive.SliderProps, "value"> & {
|
|
||||||
value: number;
|
|
||||||
label: string;
|
|
||||||
changeHandler: (value: number) => void;
|
|
||||||
}) => (
|
|
||||||
<SliderPrimitive.Root
|
|
||||||
className="slider mt-2"
|
|
||||||
value={[value]}
|
|
||||||
aria-label={label}
|
|
||||||
onValueChange={(value: number[]) => changeHandler(value[0] ?? value)}
|
|
||||||
{...props}>
|
|
||||||
<SliderPrimitive.Track className="slider-track">
|
|
||||||
<SliderPrimitive.Range className="slider-range" />
|
|
||||||
</SliderPrimitive.Track>
|
|
||||||
<SliderPrimitive.Thumb className="slider-thumb" />
|
|
||||||
</SliderPrimitive.Root>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Slider;
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import type { IframeHTMLAttributes } from "react";
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
import { InstallAppButton, AppDependencyComponent } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
||||||
@@ -37,13 +36,13 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
|
teamsPlanRequired,
|
||||||
descriptionItems,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -154,6 +153,7 @@ const Component = ({
|
|||||||
type={type}
|
type={type}
|
||||||
isProOnly={isProOnly}
|
isProOnly={isProOnly}
|
||||||
disableInstall={disableInstall}
|
disableInstall={disableInstall}
|
||||||
|
teamsPlanRequired={teamsPlanRequired}
|
||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
@@ -186,7 +186,7 @@ const Component = ({
|
|||||||
label={t("disconnect")}
|
label={t("disconnect")}
|
||||||
credentialId={existingCredentials[0]}
|
credentialId={existingCredentials[0]}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
router.replace("/apps/installed");
|
appCredentials.refetch();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -194,6 +194,7 @@ const Component = ({
|
|||||||
type={type}
|
type={type}
|
||||||
isProOnly={isProOnly}
|
isProOnly={isProOnly}
|
||||||
disableInstall={disableInstall}
|
disableInstall={disableInstall}
|
||||||
|
teamsPlanRequired={teamsPlanRequired}
|
||||||
render={({ useDefaultComponent, ...props }) => {
|
render={({ useDefaultComponent, ...props }) => {
|
||||||
if (useDefaultComponent) {
|
if (useDefaultComponent) {
|
||||||
props = {
|
props = {
|
||||||
@@ -238,16 +239,18 @@ const Component = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="prose-sm prose prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||||
<span className="text-default">
|
<span className="text-default">
|
||||||
{price === 0 ? (
|
{teamsPlanRequired ? (
|
||||||
|
t("teams_plan_required")
|
||||||
|
) : price === 0 ? (
|
||||||
t("free_to_use_apps")
|
t("free_to_use_apps")
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Intl.NumberFormat("en-US", {
|
{Intl.NumberFormat(i18n.language, {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
useGrouping: false,
|
useGrouping: false,
|
||||||
@@ -360,13 +363,14 @@ export default function App(props: {
|
|||||||
privacy?: string;
|
privacy?: string;
|
||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
|
teamsPlanRequired: AppType["teamsPlanRequired"];
|
||||||
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Shell smallHeading isPublic heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
<Shell smallHeading isPublic hideHeadingOnMobile heading={<ShellHeading />} backPath="/apps" withoutSeo>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={props.name}
|
title={props.name}
|
||||||
description={props.description}
|
description={props.description}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||||
|
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -12,12 +12,10 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
List,
|
List,
|
||||||
showToast,
|
|
||||||
AppSkeletonLoader as SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
Switch,
|
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
|
import { Calendar, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -31,83 +29,6 @@ type Props = {
|
|||||||
destinationCalendarId?: string;
|
destinationCalendarId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CalendarSwitch(props: {
|
|
||||||
type: string;
|
|
||||||
externalId: string;
|
|
||||||
title: string;
|
|
||||||
defaultSelected: boolean;
|
|
||||||
destination?: boolean;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const mutation = useMutation<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
isOn: boolean;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
async ({ isOn }) => {
|
|
||||||
const body = {
|
|
||||||
integration: props.type,
|
|
||||||
externalId: props.externalId,
|
|
||||||
};
|
|
||||||
if (isOn) {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.viewer.integrations.invalidate();
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col py-1 sm:flex-row">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.defaultSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!!props.destination && (
|
|
||||||
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
{t("adding_events_to")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CalendarList(props: Props) {
|
function CalendarList(props: Props) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||||
@@ -185,14 +106,15 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
{!fromOnboarding && (
|
{!fromOnboarding && (
|
||||||
<>
|
<>
|
||||||
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||||
<ul className="space-y-2 px-5 py-4">
|
<ul className="space-y-4 px-5 py-4">
|
||||||
{item.calendars.map((cal) => (
|
{item.calendars.map((cal) => (
|
||||||
<CalendarSwitch
|
<CalendarSwitch
|
||||||
key={cal.externalId}
|
key={cal.externalId}
|
||||||
externalId={cal.externalId}
|
externalId={cal.externalId}
|
||||||
title={cal.name || "Nameless calendar"}
|
title={cal.name || "Nameless calendar"}
|
||||||
|
name={cal.name || "Nameless calendar"}
|
||||||
type={item.integration.type}
|
type={item.integration.type}
|
||||||
defaultSelected={cal.isSelected}
|
isChecked={cal.isSelected}
|
||||||
destination={cal.externalId === props.destinationCalendarId}
|
destination={cal.externalId === props.destinationCalendarId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -264,7 +186,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||||
<>
|
<>
|
||||||
{heading && (
|
{heading && (
|
||||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||||
<ShellSubHeading
|
<ShellSubHeading
|
||||||
title={t("calendar")}
|
title={t("calendar")}
|
||||||
subtitle={t("installed_app_calendar_description")}
|
subtitle={t("installed_app_calendar_description")}
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const isAdmin = session.data?.user.role === "ADMIN";
|
||||||
|
|
||||||
if (session.status === "loading") return <></>;
|
if (session.status === "loading") return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest} actions={actions?.("block")}>
|
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||||
<div className="flex flex-col xl:flex-row">
|
<div className="flex flex-col xl:flex-row">
|
||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={AlertCircle}
|
Icon={AlertCircle}
|
||||||
headline={t("no_apps")}
|
headline={t("no_apps")}
|
||||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
description={isAdmin ? "You can enable apps in the settings" : ""}
|
||||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||||
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function InstalledAppsLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest} hideHeadingOnMobile>
|
||||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||||
{children}
|
{children}
|
||||||
</AppCategoryNavigation>
|
</AppCategoryNavigation>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { UserPermissionRole } from "@prisma/client";
|
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
@@ -6,6 +5,7 @@ import React, { useEffect } from "react";
|
|||||||
|
|
||||||
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import type Shell from "@calcom/features/shell/Shell";
|
import type Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
return (
|
return (
|
||||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
|
<Link className="text-default ml-[2px] h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
!eventLocationType.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : ""
|
||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
|||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -72,6 +72,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
slotUtcStartDate: slot.time,
|
slotUtcStartDate: slot.time,
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||||
|
bookingUid: slot.bookingUid,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import type { FC, ReactNode } from "react";
|
import type { FC, ReactNode } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
|
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { i18n } from "next-i18next";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
|
||||||
|
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { CreditCard } from "@calcom/ui/components/icon";
|
import { CreditCard } from "@calcom/ui/components/icon";
|
||||||
@@ -7,28 +7,29 @@ import { CreditCard } from "@calcom/ui/components/icon";
|
|||||||
const BookingDescriptionPayment = (props: {
|
const BookingDescriptionPayment = (props: {
|
||||||
eventType: Parameters<typeof getPaymentAppData>[0];
|
eventType: Parameters<typeof getPaymentAppData>[0];
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
|
i18n: typeof i18n;
|
||||||
}) => {
|
}) => {
|
||||||
const paymentAppData = getPaymentAppData(props.eventType);
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
amount: paymentAppData.price / 100.0,
|
||||||
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{paymentAppData.paymentOption === "HOLD" ? (
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<>
|
<>{props.t("no_show_fee_amount", params)}</>
|
||||||
{props.t("no_show_fee_amount", {
|
|
||||||
amount: paymentAppData.price / 100.0,
|
|
||||||
formatParams: { amount: { currency: paymentAppData.currency } },
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<IntlProvider locale="en">
|
<>
|
||||||
<FormattedNumber
|
{/* If undefined this will default to the browser locale */}
|
||||||
value={paymentAppData.price / 100.0}
|
{new Intl.NumberFormat(i18n?.language, {
|
||||||
style="currency"
|
style: "currency",
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
currency: paymentAppData.currency,
|
||||||
/>
|
}).format(paymentAppData.price / 100)}
|
||||||
</IntlProvider>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BookingStatus } from "@prisma/client";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@@ -10,8 +9,10 @@ import "@calcom/dayjs/locales";
|
|||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
|
import { BookingStatus } from "@calcom/prisma/enums";
|
||||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { ActionType } from "@calcom/ui";
|
import type { ActionType } from "@calcom/ui";
|
||||||
@@ -28,7 +29,7 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
|
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
@@ -86,6 +87,8 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const isTabRecurring = booking.listingStatus === "recurring";
|
const isTabRecurring = booking.listingStatus === "recurring";
|
||||||
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
|
||||||
|
|
||||||
|
const paymentAppData = getPaymentAppData(booking.eventType);
|
||||||
|
|
||||||
const bookingConfirm = async (confirm: boolean) => {
|
const bookingConfirm = async (confirm: boolean) => {
|
||||||
let body = {
|
let body = {
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
@@ -116,7 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: Slash,
|
icon: Ban,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
// For bookings with payment, only confirm if the booking is paid for
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
@@ -258,7 +261,12 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const title = booking.title;
|
const title = booking.title;
|
||||||
const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
// To be used after we run query on legacy bookings
|
||||||
|
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||||
|
|
||||||
|
const showRecordingsButtons =
|
||||||
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -272,13 +280,13 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isOpenDialog={isOpenSetLocationDialog}
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
setShowLocationModal={setIsOpenLocationDialog}
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
/>
|
/>
|
||||||
{booking.paid && (
|
{booking.paid && booking.payment[0] && (
|
||||||
<ChargeCardDialog
|
<ChargeCardDialog
|
||||||
isOpenDialog={chargeCardDialogIsOpen}
|
isOpenDialog={chargeCardDialogIsOpen}
|
||||||
setIsOpenDialog={setChargeCardDialogIsOpen}
|
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||||
bookingId={booking.id}
|
bookingId={booking.id}
|
||||||
paymentAmount={booking?.payment[0].amount}
|
paymentAmount={booking.payment[0].amount}
|
||||||
paymentCurrency={booking?.payment[0].currency}
|
paymentCurrency={booking.payment[0].currency}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showRecordingsButtons && (
|
{showRecordingsButtons && (
|
||||||
@@ -346,11 +354,15 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{booking.eventType.team.name}
|
{booking.eventType.team.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{booking.paid && (
|
{booking.paid && !booking.payment[0] ? (
|
||||||
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||||
|
{t("error_collecting_card")}
|
||||||
|
</Badge>
|
||||||
|
) : booking.paid ? (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
) : null}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
<div className="text-muted mt-2 text-sm">
|
<div className="text-muted mt-2 text-sm">
|
||||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
@@ -408,7 +420,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{title}
|
{title}
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||||
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
||||||
{t("pending_payment")}
|
{t("pending_payment")}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -450,7 +462,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
|
{booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
<TableActions actions={chargeCardActions} />
|
<TableActions actions={chargeCardActions} />
|
||||||
</div>
|
</div>
|
||||||
@@ -475,12 +487,12 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useLocale();
|
} = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const recurringCount = recurringDates.filter((date) => {
|
const recurringCount = recurringDates.filter((recurringDate) => {
|
||||||
return (
|
return (
|
||||||
date >= now &&
|
recurringDate >= now &&
|
||||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||||
.map((date) => date.toDateString())
|
.map((date) => date.toDateString())
|
||||||
.includes(date.toDateString())
|
.includes(recurringDate.toDateString())
|
||||||
);
|
);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useRouter } from "next/router";
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Button, TextArea } from "@calcom/ui";
|
import { Button, TextArea } from "@calcom/ui";
|
||||||
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||||
useTheme(props.theme);
|
|
||||||
|
|
||||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
classNames={{
|
classNames={{
|
||||||
singleValue: () => "text-default",
|
singleValue: () => "text-default",
|
||||||
dropdownIndicator: () => "text-default",
|
dropdownIndicator: () => "text-default",
|
||||||
menu: () => "!w-64 max-w-[90vw]",
|
menu: () => "!w-64 max-w-[90vw] shadow-dropdown bg-default border-subtle border rounded-md mt-1",
|
||||||
}}
|
}}
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||||
@@ -21,7 +20,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
||||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||||
@@ -38,7 +36,7 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
const PoweredByCal = dynamic(() => import("@components/ui/PoweredByCal"));
|
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
|
||||||
|
|
||||||
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||||
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
|
||||||
@@ -76,7 +74,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
brandColor: profile.brandColor,
|
brandColor: profile.brandColor,
|
||||||
darkBrandColor: profile.darkBrandColor,
|
darkBrandColor: profile.darkBrandColor,
|
||||||
});
|
});
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
@@ -105,8 +103,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
|
/*
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
@@ -114,7 +113,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]); */
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
// get dynamic user list here
|
// get dynamic user list here
|
||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
@@ -124,16 +123,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
const paymentAmount = () => {
|
|
||||||
return;
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<FormattedNumber
|
|
||||||
value={paymentAppData.price / 100.0}
|
|
||||||
style="currency"
|
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
|
||||||
/>
|
|
||||||
</IntlProvider>;
|
|
||||||
};
|
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
@@ -257,13 +247,12 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<IntlProvider locale="en">
|
<>
|
||||||
<FormattedNumber
|
{new Intl.NumberFormat(i18n.language, {
|
||||||
value={paymentAppData.price / 100.0}
|
style: "currency",
|
||||||
style="currency"
|
currency: paymentAppData.currency,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
}).format(paymentAppData.price / 100)}
|
||||||
/>
|
</>
|
||||||
</IntlProvider>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -312,7 +301,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
|
||||||
{!restProps.isBrandingHidden && !isEmbed && <PoweredByCal />}
|
{!restProps.isBrandingHidden && !isEmbed && <PoweredBy />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { createBooking, createRecurringBooking } from "@calcom/features/bookings/lib";
|
||||||
import {
|
import {
|
||||||
getBookingFieldsWithSystemFields,
|
getBookingFieldsWithSystemFields,
|
||||||
SystemField,
|
SystemField,
|
||||||
@@ -38,8 +39,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { parseDate, parseRecurringDates } from "@calcom/lib/parse-dates";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
||||||
@@ -47,9 +49,6 @@ import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/
|
|||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
|
||||||
import { parseRecurringDates, parseDate } from "@lib/parseDate";
|
|
||||||
|
|
||||||
import type { Gate, GateState } from "@components/Gates";
|
import type { Gate, GateState } from "@components/Gates";
|
||||||
import Gates from "@components/Gates";
|
import Gates from "@components/Gates";
|
||||||
@@ -231,12 +230,14 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
const reserveSlot = () => {
|
const reserveSlot = () => {
|
||||||
if (queryDuration) {
|
if (queryDuration) {
|
||||||
reserveSlotMutation.mutate({
|
reserveSlotMutation.mutate({
|
||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||||
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||||
|
bookingUid: currentSlotBooking?.uid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -252,13 +253,13 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
/* if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
telemetryEventTypes.embedView,
|
telemetryEventTypes.embedView,
|
||||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
reserveSlot();
|
reserveSlot();
|
||||||
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -433,7 +434,6 @@ const BookingPage = ({
|
|||||||
// Calculate the booking date(s)
|
// Calculate the booking date(s)
|
||||||
let recurringStrings: string[] = [],
|
let recurringStrings: string[] = [],
|
||||||
recurringDates: Date[] = [];
|
recurringDates: Date[] = [];
|
||||||
|
|
||||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||||
{
|
{
|
||||||
@@ -443,7 +443,7 @@ const BookingPage = ({
|
|||||||
recurringCount: parseInt(recurringEventCount.toString()),
|
recurringCount: parseInt(recurringEventCount.toString()),
|
||||||
selectedTimeFormat: timeFormat,
|
selectedTimeFormat: timeFormat,
|
||||||
},
|
},
|
||||||
i18n
|
i18n.language
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,16 +547,16 @@ const BookingPage = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main",
|
"main",
|
||||||
isBackgroundTransparent ? "" : "bg-default",
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
|
||||||
"border-booker sm:border-booker-width rounded-md"
|
"border-booker sm:border-booker-width rounded-md"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<BookingDescriptionPayment eventType={eventType} t={t} />
|
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
{!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]" />
|
<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">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{getEveryFreqFor({
|
{getEveryFreqFor({
|
||||||
@@ -572,7 +572,7 @@ const BookingPage = ({
|
|||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
`${parseDate(date, i18n, timeFormat)}`}
|
`${parseDate(date, i18n.language, { selectedTimeFormat: timeFormat })}`}
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
!rescheduleUid &&
|
!rescheduleUid &&
|
||||||
eventType.recurringEvent?.freq &&
|
eventType.recurringEvent?.freq &&
|
||||||
@@ -602,7 +602,9 @@ const BookingPage = ({
|
|||||||
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
parseDate(dayjs(booking.startTime), i18n.language, {
|
||||||
|
selectedTimeFormat: timeFormat,
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Trans } from "next-i18next";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { IntlProvider, FormattedNumber } from "react-intl";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -40,6 +38,11 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currencyStringParams = {
|
||||||
|
amount: props.paymentAmount / 100.0,
|
||||||
|
formatParams: { amount: { currency: props.paymentCurrency } },
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -49,19 +52,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("charge_card")} />
|
<DialogHeader title={t("charge_card")} />
|
||||||
<Trans i18nKey="charge_card_dialog_body">
|
<p>{t("charge_card_dialog_body", currencyStringParams)}</p>
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
You are about to charge the attendee{" "}
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<FormattedNumber
|
|
||||||
value={props.paymentAmount / 100.0}
|
|
||||||
style="currency"
|
|
||||||
currency={props.paymentCurrency?.toUpperCase()}
|
|
||||||
/>
|
|
||||||
</IntlProvider>
|
|
||||||
. Are you sure you want to continue?
|
|
||||||
</p>
|
|
||||||
</Trans>
|
|
||||||
|
|
||||||
{chargeError && (
|
{chargeError && (
|
||||||
<div className="mt-4 flex text-red-500">
|
<div className="mt-4 flex text-red-500">
|
||||||
@@ -80,16 +71,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
|||||||
bookingId,
|
bookingId,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
<Trans i18nKey="charge_card_confirm">
|
{t("charge_attendee", currencyStringParams)}
|
||||||
Charge attendee{" "}
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<FormattedNumber
|
|
||||||
value={props.paymentAmount / 100.0}
|
|
||||||
style="currency"
|
|
||||||
currency={props.paymentCurrency?.toUpperCase()}
|
|
||||||
/>
|
|
||||||
</IntlProvider>
|
|
||||||
</Trans>
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
id="locationInput"
|
id="locationInput"
|
||||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||||
required
|
required
|
||||||
className="border-default block w-full rounded-sm text-sm"
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
defaultValue={
|
defaultValue={
|
||||||
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
statusCode?: number | null;
|
statusCode?: number | null;
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import { getEventName } from "@calcom/core/event";
|
|||||||
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import {
|
||||||
|
allowDisablingAttendeeConfirmationEmails,
|
||||||
|
allowDisablingHostConfirmationEmails,
|
||||||
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -43,6 +47,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
const bookingFields: Prisma.JsonObject = {};
|
const bookingFields: Prisma.JsonObject = {};
|
||||||
|
|
||||||
|
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||||
|
|
||||||
eventType.bookingFields.forEach(({ name }) => {
|
eventType.bookingFields.forEach(({ name }) => {
|
||||||
bookingFields[name] = name + " input";
|
bookingFields[name] = name + " input";
|
||||||
});
|
});
|
||||||
@@ -316,7 +322,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
disabled={seatsLocked.disabled}
|
disabled={seatsLocked.disabled}
|
||||||
defaultValue={value || 2}
|
defaultValue={value || 2}
|
||||||
min={1}
|
min={1}
|
||||||
className="w-24"
|
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(Math.abs(Number(e.target.value)));
|
onChange(Math.abs(Number(e.target.value)));
|
||||||
@@ -338,7 +343,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{allowDisablingAttendeeConfirmationEmails(workflows) && (
|
||||||
|
<>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<Controller
|
||||||
|
name="metadata.disableStandardEmails.confirmation.attendee"
|
||||||
|
control={formMethods.control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("disable_attendees_confirmation_emails")}
|
||||||
|
description={t("disable_attendees_confirmation_emails_description")}
|
||||||
|
checked={value || false}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{allowDisablingHostConfirmationEmails(workflows) && (
|
||||||
|
<>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<Controller
|
||||||
|
name="metadata.disableStandardEmails.confirmation.host"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("disable_host_confirmation_emails")}
|
||||||
|
description={t("disable_host_confirmation_emails_description")}
|
||||||
|
checked={value || false}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
|
||||||
|
onChange(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<CustomEventTypeModal
|
<CustomEventTypeModal
|
||||||
close={closeEventNameTip}
|
close={closeEventNameTip}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
{!shouldLockDisableProps("apps").disabled && (
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
<div>
|
<div>
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
|
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
{notInstalledApps?.map((app) => (
|
{notInstalledApps?.map((app) => (
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
|
import { useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import type { OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
|
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AvailabilityOption = {
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
export type AvailabilityOption = {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
@@ -62,40 +64,12 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailabilitySelect = ({
|
|
||||||
className = "",
|
|
||||||
options,
|
|
||||||
value,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
value: AvailabilityOption | undefined;
|
|
||||||
options: AvailabilityOption[];
|
|
||||||
isDisabled?: boolean;
|
|
||||||
onBlur: () => void;
|
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
|
||||||
}) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
placeholder={t("select")}
|
|
||||||
options={options}
|
|
||||||
isDisabled={props.isDisabled}
|
|
||||||
isSearchable={false}
|
|
||||||
onChange={props.onChange}
|
|
||||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
|
||||||
defaultValue={value}
|
|
||||||
components={{ Option, SingleValue }}
|
|
||||||
isMulti={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const format = (date: Date, hour12: boolean) =>
|
const format = (date: Date, hour12: boolean) =>
|
||||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
|
Intl.DateTimeFormat(undefined, {
|
||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
hour: "numeric",
|
||||||
);
|
minute: "numeric",
|
||||||
|
hourCycle: hour12 ? "h12" : "h24",
|
||||||
|
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
|
||||||
|
|
||||||
const EventTypeScheduleDetails = ({
|
const EventTypeScheduleDetails = ({
|
||||||
isManagedEventType,
|
isManagedEventType,
|
||||||
@@ -145,7 +119,7 @@ const EventTypeScheduleDetails = ({
|
|||||||
{format(dayRange.startTime, timeFormat === 12)}
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
</span>
|
</span>
|
||||||
<span className="ms-4">-</span>
|
<span className="ms-4">-</span>
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -180,69 +154,66 @@ const EventTypeScheduleDetails = ({
|
|||||||
|
|
||||||
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
|
const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
useLockedFieldsManager(
|
eventType,
|
||||||
eventType,
|
t("locked_fields_admin_description"),
|
||||||
t("locked_fields_admin_description"),
|
t("locked_fields_member_description")
|
||||||
t("locked_fields_member_description")
|
);
|
||||||
);
|
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
const watchSchedule = watch("schedule");
|
const watchSchedule = watch("schedule");
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
|
||||||
|
onSuccess: ({ schedules }) => {
|
||||||
|
const options = schedules.map((schedule) => ({
|
||||||
|
value: schedule.id,
|
||||||
|
label: schedule.name,
|
||||||
|
isDefault: schedule.isDefault,
|
||||||
|
isManaged: false,
|
||||||
|
}));
|
||||||
|
|
||||||
if (!data?.schedules.length && !isLoading)
|
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||||
return (
|
if (isManagedEventType) {
|
||||||
<EmptyScreen
|
options.push({
|
||||||
Icon={Clock}
|
value: 0,
|
||||||
headline={t("new_schedule_heading")}
|
label: t("members_default_schedule"),
|
||||||
description={t("new_schedule_description")}
|
isDefault: false,
|
||||||
buttonRaw={<NewScheduleButton fromEventType />}
|
isManaged: false,
|
||||||
border={false}
|
});
|
||||||
/>
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const schedules = data?.schedules || [];
|
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
||||||
|
// the managed schedule option
|
||||||
|
if (
|
||||||
|
isChildrenManagedEventType &&
|
||||||
|
watchSchedule &&
|
||||||
|
!schedules.find((schedule) => schedule.id === watchSchedule)
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
value: watchSchedule,
|
||||||
|
label: eventType.scheduleName ?? t("default_schedule_name"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
setOptions(options);
|
||||||
value: schedule.id,
|
|
||||||
label: schedule.name,
|
|
||||||
isDefault: schedule.isDefault,
|
|
||||||
isManaged: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
const scheduleId = formMethods.getValues("schedule");
|
||||||
if (isManagedEventType) {
|
const value = options.find((option) =>
|
||||||
options.push({
|
scheduleId
|
||||||
value: 0,
|
? option.value === scheduleId
|
||||||
label: t("members_default_schedule"),
|
: isManagedEventType
|
||||||
isDefault: false,
|
? option.value === 0
|
||||||
isManaged: false,
|
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
formMethods.setValue("availability", value);
|
||||||
// the managed schedule option
|
},
|
||||||
if (
|
});
|
||||||
isChildrenManagedEventType &&
|
|
||||||
watchSchedule &&
|
|
||||||
!schedules.find((schedule) => schedule.id === watchSchedule)
|
|
||||||
) {
|
|
||||||
options.push({
|
|
||||||
value: watchSchedule,
|
|
||||||
label: eventType.scheduleName ?? t("default_schedule_name"),
|
|
||||||
isDefault: false,
|
|
||||||
isManaged: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = options.find((option) =>
|
const availabilityValue = formMethods.watch("availability");
|
||||||
watchSchedule
|
|
||||||
? option.value === watchSchedule
|
|
||||||
: isManagedEventType
|
|
||||||
? option.value === 0
|
|
||||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -251,27 +222,33 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
{t("availability")}
|
{t("availability")}
|
||||||
{shouldLockIndicator("availability")}
|
{shouldLockIndicator("availability")}
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
{isLoading && <SelectSkeletonLoader />}
|
||||||
name="schedule"
|
{!isLoading && (
|
||||||
render={({ field }) => (
|
<Controller
|
||||||
<>
|
name="schedule"
|
||||||
<AvailabilitySelect
|
render={({ field }) => {
|
||||||
value={value}
|
return (
|
||||||
options={options}
|
<Select
|
||||||
onBlur={field.onBlur}
|
placeholder={t("select")}
|
||||||
isDisabled={shouldLockDisableProps("schedule").disabled}
|
options={options}
|
||||||
name={field.name}
|
isSearchable={false}
|
||||||
onChange={(selected) => {
|
onChange={(selected) => {
|
||||||
field.onChange(selected?.value || null);
|
field.onChange(selected?.value || null);
|
||||||
}}
|
if (selected?.value) formMethods.setValue("availability", selected);
|
||||||
/>
|
}}
|
||||||
</>
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
)}
|
value={availabilityValue}
|
||||||
/>
|
components={{ Option, SingleValue }}
|
||||||
|
isMulti={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{value?.value !== 0 ? (
|
{availabilityValue?.value !== 0 ? (
|
||||||
<EventTypeScheduleDetails
|
<EventTypeScheduleDetails
|
||||||
selectedScheduleValue={value}
|
selectedScheduleValue={availabilityValue}
|
||||||
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
|||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
import findDurationType from "@calcom/lib/findDurationType";
|
import findDurationType from "@calcom/lib/findDurationType";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
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 type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
import { Plus, Trash } from "@calcom/ui/components/icon";
|
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
@@ -108,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
const PERIOD_TYPES = [
|
const PERIOD_TYPES = [
|
||||||
@@ -149,12 +149,25 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||||
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||||
const periodTypeLocked = shouldLockDisableProps("periodType");
|
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||||
|
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||||
|
|
||||||
const optionsPeriod = [
|
const optionsPeriod = [
|
||||||
{ value: 1, label: t("calendar_days") },
|
{ value: 1, label: t("calendar_days") },
|
||||||
{ value: 0, label: t("business_days") },
|
{ value: 0, label: t("business_days") },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// offsetStart toggle is client-side only, opened by default if offsetStart is set
|
||||||
|
const offsetStartValue = useWatch({
|
||||||
|
control: formMethods.control,
|
||||||
|
name: "offsetStart",
|
||||||
|
});
|
||||||
|
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
|
||||||
|
|
||||||
|
// Preview how the offset will affect start times
|
||||||
|
const offsetOriginalTime = new Date();
|
||||||
|
offsetOriginalTime.setHours(9, 0, 0, 0);
|
||||||
|
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4 lg:space-y-8">
|
<div className="space-y-4 lg:space-y-8">
|
||||||
@@ -432,6 +445,32 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<hr className="border-subtle" />
|
||||||
|
<SettingsToggle
|
||||||
|
title={t("offset_toggle")}
|
||||||
|
description={t("offset_toggle_description")}
|
||||||
|
{...offsetStartLockedProps}
|
||||||
|
checked={offsetToggle}
|
||||||
|
onCheckedChange={(active) => {
|
||||||
|
setOffsetToggle(active);
|
||||||
|
if (!active) {
|
||||||
|
formMethods.setValue("offsetStart", 0);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
{...offsetStartLockedProps}
|
||||||
|
label={t("offset_start")}
|
||||||
|
defaultValue={eventType.offsetStart}
|
||||||
|
{...formMethods.register("offsetStart")}
|
||||||
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
|
hint={t("offset_start_description", {
|
||||||
|
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||||
|
adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { z } from "zod";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import cx from "@calcom/lib/classNames";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
@@ -80,6 +81,7 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
|||||||
const [mounted, setIsMounted] = useState(false);
|
const [mounted, setIsMounted] = useState(false);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { description } = props;
|
const { description } = props;
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -91,6 +93,8 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
|||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
placeholder={t("quick_video_meeting")}
|
placeholder={t("quick_video_meeting")}
|
||||||
editable={props.editable}
|
editable={props.editable}
|
||||||
|
firstRender={firstRender}
|
||||||
|
setFirstRender={setFirstRender}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -272,12 +276,18 @@ export const EventSetupTab = (
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={`${location.type}${index}`}
|
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 justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-4 w-4"
|
className={cx(
|
||||||
|
"h-4 w-4",
|
||||||
|
// invert all the icons except app icons
|
||||||
|
eventLocationType.iconUrl &&
|
||||||
|
!eventLocationType.iconUrl.startsWith("/app-store") &&
|
||||||
|
"dark:invert"
|
||||||
|
)}
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
||||||
@@ -380,7 +390,12 @@ export const EventSetupTab = (
|
|||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
|
||||||
{team ? "team/" + team.slug : eventType.users[0].username}/
|
{!isManagedEventType
|
||||||
|
? team
|
||||||
|
? "team/" + team.slug
|
||||||
|
: eventType.users[0].username
|
||||||
|
: t("username_placeholder")}
|
||||||
|
/
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{...formMethods.register("slug", {
|
{...formMethods.register("slug", {
|
||||||
@@ -456,6 +471,7 @@ export const EventSetupTab = (
|
|||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
{...formMethods.register("length")}
|
{...formMethods.register("length")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
|
min={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!lengthLockedProps.disabled && (
|
{!lengthLockedProps.disabled && (
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { ComponentProps } 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 ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IUserToValue {
|
interface IUserToValue {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export const EventTeamWebhooksTab = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
{/* Edit webhook dialog */}
|
{/* Edit webhook dialog */}
|
||||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
<DialogContent title={t("edit_webhook")}>
|
<DialogContent enableOverflow title={t("edit_webhook")}>
|
||||||
<WebhookForm
|
<WebhookForm
|
||||||
webhook={webhookToEdit}
|
webhook={webhookToEdit}
|
||||||
apps={installedApps?.items.map((app) => app.slug)}
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import { Webhook as TbWebhook } from "lucide-react";
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
import { Trans } 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 { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -63,6 +64,7 @@ type Props = {
|
|||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
formMethods: UseFormReturn<FormValues>;
|
formMethods: UseFormReturn<FormValues>;
|
||||||
isUpdateMutationLoading?: boolean;
|
isUpdateMutationLoading?: boolean;
|
||||||
|
availability?: AvailabilityOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNavigation(props: {
|
function getNavigation(props: {
|
||||||
@@ -71,8 +73,10 @@ function getNavigation(props: {
|
|||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||||
|
props;
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -128,6 +132,7 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
isUpdateMutationLoading,
|
isUpdateMutationLoading,
|
||||||
formMethods,
|
formMethods,
|
||||||
|
availability,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -165,12 +170,13 @@ function EventTypeSingleLayout({
|
|||||||
|
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
let navigation = getNavigation({
|
const navigation = getNavigation({
|
||||||
t,
|
t,
|
||||||
eventType,
|
eventType,
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
});
|
});
|
||||||
navigation.splice(1, 0, {
|
navigation.splice(1, 0, {
|
||||||
name: "availability",
|
name: "availability",
|
||||||
@@ -204,7 +210,7 @@ function EventTypeSingleLayout({
|
|||||||
}
|
}
|
||||||
if (isManagedEventType || isChildrenManagedEventType) {
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
// Removing apps and workflows for manageg event types by admins v1
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
navigation = navigation.slice(0, -2);
|
navigation.splice(-2, 1);
|
||||||
} else {
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
@@ -214,7 +220,7 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
||||||
|
|
||||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
@@ -232,7 +238,7 @@ function EventTypeSingleLayout({
|
|||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{!eventType.metadata.managedEventConfig && (
|
{!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
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
@@ -374,6 +380,8 @@ function EventTypeSingleLayout({
|
|||||||
tabs={EventTypeTabs}
|
tabs={EventTypeTabs}
|
||||||
sticky
|
sticky
|
||||||
linkProps={{ shallow: true }}
|
linkProps={{ shallow: true }}
|
||||||
|
itemClassname="items-start"
|
||||||
|
iconClassName="md:mt-px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||||
|
|||||||
+7
-5
@@ -3,15 +3,16 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { App } from "@calcom/types/App";
|
import type { App } from "@calcom/types/App";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
interface ICalendarItem {
|
interface IAppConnectionItem {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
|
installed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalendarItem = (props: ICalendarItem) => {
|
const AppConnectionItem = (props: IAppConnectionItem) => {
|
||||||
const { title, logo, type } = props;
|
const { title, logo, type, installed } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center justify-between p-5">
|
<div className="flex flex-row items-center justify-between p-5">
|
||||||
@@ -25,13 +26,14 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||||||
<Button
|
<Button
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
disabled={installed}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Save cookie key to return url step
|
// Save cookie key to return url step
|
||||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||||
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
||||||
}}>
|
}}>
|
||||||
{t("connect")}
|
{installed ? t("installed") : t("connect")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -39,4 +41,4 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { CalendarItem };
|
export { AppConnectionItem };
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CalendarSwitch } from "./CalendarSwitch";
|
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||||
|
|
||||||
interface IConnectedCalendarItem {
|
interface IConnectedCalendarItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
|
||||||
|
|
||||||
|
export function StepConnectionLoader() {
|
||||||
|
return (
|
||||||
|
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
||||||
|
{Array.from({ length: 4 }).map((_item, index) => {
|
||||||
|
return (
|
||||||
|
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
||||||
|
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
||||||
|
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
||||||
|
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { SkeletonAvatar, SkeletonText, SkeletonButton, List } from "@calcom/ui";
|
import { List } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { CalendarItem } from "../components/CalendarItem";
|
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||||
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
|
||||||
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
|
import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
|
||||||
|
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||||
|
|
||||||
interface IConnectCalendarsProps {
|
interface IConnectCalendarsProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
@@ -33,7 +33,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
firstCalendar.integration.title &&
|
firstCalendar.integration.title &&
|
||||||
firstCalendar.integration.logo && (
|
firstCalendar.integration.logo && (
|
||||||
<>
|
<>
|
||||||
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black">
|
<List className="bg-default border-subtle rounded-md border p-0 dark:bg-black ">
|
||||||
<ConnectedCalendarItem
|
<ConnectedCalendarItem
|
||||||
key={firstCalendar.integration.title}
|
key={firstCalendar.integration.title}
|
||||||
name={firstCalendar.integration.title}
|
name={firstCalendar.integration.title}
|
||||||
@@ -60,7 +60,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
queryIntegrations.data.items.map((item) => (
|
queryIntegrations.data.items.map((item) => (
|
||||||
<li key={item.title}>
|
<li key={item.title}>
|
||||||
{item.title && item.logo && (
|
{item.title && item.logo && (
|
||||||
<CalendarItem
|
<AppConnectionItem
|
||||||
type={item.type}
|
type={item.type}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
@@ -72,19 +72,8 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryIntegrations.isLoading && (
|
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
||||||
<ul className="bg-default divide-subtle border-subtle divide-y rounded-md border p-0 dark:bg-black">
|
|
||||||
{[0, 0, 0, 0].map((_item, index) => {
|
|
||||||
return (
|
|
||||||
<li className="flex w-full flex-row justify-center border-b-0 py-6" key={index}>
|
|
||||||
<SkeletonAvatar className="mx-6 h-8 w-8 px-4" />
|
|
||||||
<SkeletonText className="ml-1 mr-4 mt-3 h-5 w-full" />
|
|
||||||
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="save-calendar-button"
|
data-testid="save-calendar-button"
|
||||||
@@ -95,7 +84,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
onClick={() => nextStep()}
|
onClick={() => nextStep()}
|
||||||
disabled={disabledNextButton}>
|
disabled={disabledNextButton}>
|
||||||
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { List } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import { AppConnectionItem } from "../components/AppConnectionItem";
|
||||||
|
import { StepConnectionLoader } from "../components/StepConnectionLoader";
|
||||||
|
|
||||||
|
interface ConnectedAppStepProps {
|
||||||
|
nextStep: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||||
|
const { nextStep } = props;
|
||||||
|
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||||
|
variant: "conferencing",
|
||||||
|
onlyInstalled: false,
|
||||||
|
});
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
|
||||||
|
(item) => item.credentialIds.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!isLoading && (
|
||||||
|
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||||
|
{queryConnectedVideoApps?.items &&
|
||||||
|
queryConnectedVideoApps?.items.map((item) => {
|
||||||
|
if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
|
||||||
|
return (
|
||||||
|
<li key={item.name}>
|
||||||
|
{item.name && item.logo && (
|
||||||
|
<AppConnectionItem
|
||||||
|
type={item.type}
|
||||||
|
title={item.name}
|
||||||
|
description={item.description}
|
||||||
|
logo={item.logo}
|
||||||
|
installed={item.credentialIds.length > 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && <StepConnectionLoader />}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="save-video-button"
|
||||||
|
className={classNames(
|
||||||
|
"text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm",
|
||||||
|
!hasAnyInstalledVideoApps ? "cursor-not-allowed opacity-20" : ""
|
||||||
|
)}
|
||||||
|
disabled={!hasAnyInstalledVideoApps}
|
||||||
|
onClick={() => nextStep()}>
|
||||||
|
{t("next_step_text")}
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ConnectedVideoStep };
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
@@ -9,6 +8,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Form } from "@calcom/ui";
|
import { Button, Form } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface ISetupAvailabilityProps {
|
interface ISetupAvailabilityProps {
|
||||||
nextStep: () => void;
|
nextStep: () => void;
|
||||||
@@ -50,7 +50,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
className="bg-default dark:text-inverted text-emphasis w-full dark:bg-opacity-5"
|
className="bg-default dark:text-inverted text-emphasis w-full [--cal-brand-accent:#fafafa] dark:bg-opacity-5"
|
||||||
form={availabilityForm}
|
form={availabilityForm}
|
||||||
handleSubmit={async (values) => {
|
handleSubmit={async (values) => {
|
||||||
try {
|
try {
|
||||||
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||||
disabled={availabilityForm.formState.isSubmitting}>
|
disabled={availabilityForm.formState.isSubmitting}>
|
||||||
{t("next_step_text")} <ArrowRightIcon className="ml-2 h-4 w-4 self-center" />
|
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FormEvent } from "react";
|
import type { FormEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
const createEventType = trpc.viewer.eventTypes.create.useMutation();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
|
|
||||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: async (_data, context) => {
|
onSuccess: async (_data, context) => {
|
||||||
@@ -147,6 +148,8 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||||
setText={(value: string) => setValue("bio", turndown(value))}
|
setText={(value: string) => setValue("bio", turndown(value))}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
|
firstRender={firstRender}
|
||||||
|
setFirstRender={setFirstRender}
|
||||||
/>
|
/>
|
||||||
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
<p className="dark:text-inverted text-default mt-2 font-sans text-sm font-normal">
|
||||||
{t("few_sentences_about_yourself")}
|
{t("few_sentences_about_yourself")}
|
||||||
@@ -156,7 +159,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
|
||||||
{t("finish")}
|
{t("finish")}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -10,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TimezoneSelect } from "@calcom/ui";
|
import { Button, TimezoneSelect } from "@calcom/ui";
|
||||||
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
className="mt-8 flex w-full flex-row justify-center"
|
className="mt-8 flex w-full flex-row justify-center"
|
||||||
disabled={mutation.isLoading}>
|
disabled={mutation.isLoading}>
|
||||||
{t("next_step_text")}
|
{t("next_step_text")}
|
||||||
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Badge, showToast, Switch } from "@calcom/ui";
|
|
||||||
import { ArrowLeft } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
|
||||||
type: string;
|
|
||||||
externalId: string;
|
|
||||||
title: string;
|
|
||||||
defaultSelected: boolean;
|
|
||||||
isSelected: boolean;
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const mutation = useMutation<
|
|
||||||
unknown,
|
|
||||||
unknown,
|
|
||||||
{
|
|
||||||
isOn: boolean;
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
async ({ isOn }) => {
|
|
||||||
const body = {
|
|
||||||
integration: props.type,
|
|
||||||
externalId: props.externalId,
|
|
||||||
};
|
|
||||||
if (isOn) {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const res = await fetch("/api/availability/calendar", {
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error("Something went wrong");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
async onSettled() {
|
|
||||||
await utils.viewer.integrations.invalidate();
|
|
||||||
},
|
|
||||||
onError() {
|
|
||||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-2 py-1 rtl:space-x-reverse">
|
|
||||||
<Switch
|
|
||||||
key={props.externalId}
|
|
||||||
name="enabled"
|
|
||||||
label={props.title}
|
|
||||||
defaultChecked={props.isSelected}
|
|
||||||
onCheckedChange={(isOn: boolean) => {
|
|
||||||
mutation.mutate({ isOn });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{props.defaultSelected && (
|
|
||||||
<Badge variant="gray">
|
|
||||||
<div className="flex">
|
|
||||||
<ArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
{!isBioEmpty ? (
|
{!isBioEmpty ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useSession } from "next-auth/react";
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
type AdminRequiredProps = {
|
|
||||||
as?: keyof JSX.IntrinsicElements;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated use PermssionContainer instead. Will delete once V2 goes live */
|
|
||||||
export const AdminRequired: FC<AdminRequiredProps> = ({ children, as, ...rest }) => {
|
|
||||||
const session = useSession();
|
|
||||||
|
|
||||||
if (session.data?.user.role !== "ADMIN") return null;
|
|
||||||
const Component = as ?? Fragment;
|
|
||||||
return <Component {...rest}>{children}</Component>;
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
import { HeadSeo, Logo } from "@calcom/ui";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
|
|
||||||
@@ -18,10 +17,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
|
<div className="flex min-h-screen flex-col justify-center bg-[#f3f4f6] py-12 sm:px-6 lg:px-8">
|
||||||
<HeadSeo title={props.title} description={props.description} />
|
<HeadSeo title={props.title} description={props.description} />
|
||||||
{props.showLogo && (
|
{props.showLogo && <Logo small inline={false} className="mx-auto mb-auto" />}
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
|
||||||
{props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
|
{props.heading && <h2 className="font-cal text-emphasis text-center text-3xl">{props.heading}</h2>}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ const EditableHeading = function EditableHeading({
|
|||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const enableEditing = () => setIsEditing(true);
|
const enableEditing = () => setIsEditing(true);
|
||||||
return (
|
return (
|
||||||
<div className="group relative cursor-pointer" onClick={enableEditing}>
|
<div
|
||||||
<div className="flex items-center">
|
className="group pointer-events-none relative truncate sm:pointer-events-auto"
|
||||||
|
onClick={enableEditing}>
|
||||||
|
<div className="flex cursor-pointer items-center">
|
||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
@@ -29,7 +31,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
value={value}
|
value={value}
|
||||||
required
|
required
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer truncate border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
||||||
)}
|
)}
|
||||||
onFocus={(e) => {
|
onFocus={(e) => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
|
||||||
import { POWERED_BY_URL } from "@calcom/lib/constants";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
|
|
||||||
const PoweredByCal = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const isEmbed = useIsEmbed();
|
|
||||||
return (
|
|
||||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
|
||||||
<Link href={POWERED_BY_URL} target="_blank" className="text-subtle opacity-50 hover:opacity-100">
|
|
||||||
{t("powered_by")}{" "}
|
|
||||||
<img
|
|
||||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
|
||||||
src="/cal-logo-word.svg"
|
|
||||||
alt="Cal.com Logo"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
|
||||||
src="/cal-logo-word-dark.svg"
|
|
||||||
alt="Cal.com Logo"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PoweredByCal;
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { debounce, noop } from "lodash";
|
import { debounce, noop } from "lodash";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -15,6 +14,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
|
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
||||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
@@ -195,7 +195,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isInputUsernamePremium ? "border border-orange-400 " : "",
|
isInputUsernamePremium ? "border border-orange-400 " : "",
|
||||||
"border-default bg-muted text-subtle hidden h-9 items-center rounded-l-md border border-r-0 border-r-gray-300 px-3 text-sm md:inline-flex"
|
"border-default bg-muted text-subtle hidden h-9 items-center rounded-l-md border border-r-0 px-3 text-sm md:inline-flex"
|
||||||
)}>
|
)}>
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
||||||
</span>
|
</span>
|
||||||
@@ -215,7 +215,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
: "border focus:border",
|
: "border focus:border",
|
||||||
markAsError
|
markAsError
|
||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none"
|
||||||
: "border-l-gray-300",
|
: "border-l-default",
|
||||||
disabled ? "bg-subtle text-muted focus:border-0" : ""
|
disabled ? "bg-subtle text-muted focus:border-0" : ""
|
||||||
)}
|
)}
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
@@ -230,12 +230,16 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<div className="absolute top-0 right-2 flex flex-row">
|
<div className="absolute top-0 right-2 flex flex-row">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mx-2 py-1",
|
"mx-2 py-2",
|
||||||
isInputUsernamePremium ? "text-orange-400" : "",
|
isInputUsernamePremium ? "text-transparent" : "",
|
||||||
usernameIsAvailable ? "" : ""
|
usernameIsAvailable ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
|
||||||
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
|
{!isInputUsernamePremium && usernameIsAvailable ? (
|
||||||
|
<Check className="mt-[2px] h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="rlt:space-x-reverse ms-2 me-2 mt-px flex flex-row space-x-2 rtl:space-x-reverse">
|
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
|
"mb-0 mt-0 rounded-md rounded-l-none",
|
||||||
markAsError
|
markAsError
|
||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||||
: ""
|
: ""
|
||||||
@@ -136,8 +136,8 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
/>
|
/>
|
||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||||
<span className={classNames("mx-2 py-2")}>
|
<span className={classNames("mx-2 py-2.5")}>
|
||||||
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
|
{usernameIsAvailable ? <Check className="h-4 w-4" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
|||||||
{...rest}
|
{...rest}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="text-primary-600 focus:ring-primary-500 border-default h-4 w-4 rounded"
|
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ms-3 text-sm">{description}</span>
|
<span className="ms-3 text-sm">{description}</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { components } from "react-select";
|
|||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import cx from "@calcom/lib/classNames";
|
||||||
import { Select } from "@calcom/ui";
|
import { Select } from "@calcom/ui";
|
||||||
|
|
||||||
export type LocationOption = {
|
export type LocationOption = {
|
||||||
@@ -19,10 +20,14 @@ export type GroupOptionType = GroupBase<LocationOption>;
|
|||||||
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* TODO: figure out a way to invert icons when in dark mode. We can't just
|
{icon && (
|
||||||
dark:invert due to google meet cal etc all breaking when we do this
|
<img
|
||||||
*/}
|
src={icon}
|
||||||
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5 dark:hidden" />}
|
alt="cover"
|
||||||
|
// invert all the icons except app icons
|
||||||
|
className={cx("h-3.5 w-3.5", icon && !icon.startsWith("/app-store") && "dark:invert")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<span className={classNames("text-sm font-medium")}>{label}</span>
|
<span className={classNames("text-sm font-medium")}>{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+123
-37
@@ -19,16 +19,23 @@ import { MetaProvider } from "@calcom/ui";
|
|||||||
import usePublicPage from "@lib/hooks/usePublicPage";
|
import usePublicPage from "@lib/hooks/usePublicPage";
|
||||||
import type { WithNonceProps } from "@lib/withNonce";
|
import type { WithNonceProps } from "@lib/withNonce";
|
||||||
|
|
||||||
|
import { useViewerI18n } from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
|
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
|
||||||
({ children }) => <>{children}</>
|
({ children }) => <>{children}</>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
export type AppProps = Omit<
|
||||||
|
NextAppProps<WithNonceProps & { themeBasis?: string } & Record<string, unknown>>,
|
||||||
|
"Component"
|
||||||
|
> & {
|
||||||
Component: NextAppProps["Component"] & {
|
Component: NextAppProps["Component"] & {
|
||||||
requiresLicense?: boolean;
|
requiresLicense?: boolean;
|
||||||
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
isThemeSupported?: boolean;
|
||||||
|
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||||
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
||||||
|
PageWrapper?: (props: AppProps) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
@@ -44,9 +51,7 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||||
* We intend to not cache i18n query
|
* We intend to not cache i18n query
|
||||||
**/
|
**/
|
||||||
const { i18n, locale } = trpc.viewer.public.i18n.useQuery(undefined, {
|
const { i18n, locale } = useViewerI18n().data ?? {
|
||||||
trpc: { context: { skipBatch: true } },
|
|
||||||
}).data ?? {
|
|
||||||
locale: "en",
|
locale: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,46 +66,30 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||||||
return <I18nextAdapter {...passedProps} />;
|
return <I18nextAdapter {...passedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CalcomThemeProvider = (
|
const enum ThemeSupport {
|
||||||
props: PropsWithChildren<
|
// e.g. Login Page
|
||||||
WithNonceProps & { isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean) }
|
None = "none",
|
||||||
>
|
// Entire App except Booking Pages
|
||||||
) => {
|
App = "systemOnly",
|
||||||
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
// Booking Pages(including Routing Forms)
|
||||||
// undefined or true means we use system theme
|
Booking = "userConfigured",
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalcomThemeProps = PropsWithChildren<
|
||||||
|
Pick<AppProps["pageProps"], "nonce" | "themeBasis"> &
|
||||||
|
Pick<AppProps["Component"], "isBookingPage" | "isThemeSupported">
|
||||||
|
>;
|
||||||
|
const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isThemeSupported = (() => {
|
|
||||||
if (typeof props.isThemeSupported === "function") {
|
|
||||||
return props.isThemeSupported({ router: router });
|
|
||||||
}
|
|
||||||
if (typeof props.isThemeSupported === "undefined") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return props.isThemeSupported;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const forcedTheme = !isThemeSupported ? "light" : undefined;
|
|
||||||
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||||
const isEmbedMode = typeof embedNamespace === "string";
|
const isEmbedMode = typeof embedNamespace === "string";
|
||||||
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
|
|
||||||
// This is done to ensure that the default theme is light when we force light mode and as soon as you navigate to a page that is dark we dont need a hard refresh to change
|
|
||||||
const storageKey = isEmbedMode
|
|
||||||
? `embed-theme-${embedNamespace}`
|
|
||||||
: !isThemeSupported
|
|
||||||
? "cal-light"
|
|
||||||
: "theme";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace, router })}>
|
||||||
nonce={props.nonce}
|
|
||||||
enableColorScheme={false}
|
|
||||||
enableSystem={isThemeSupported}
|
|
||||||
forcedTheme={forcedTheme}
|
|
||||||
storageKey={storageKey}
|
|
||||||
attribute="class">
|
|
||||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||||
{typeof window !== "undefined" && !isEmbedMode && (
|
{typeof window !== "undefined" && !isEmbedMode && (
|
||||||
@@ -117,6 +106,100 @@ const CalcomThemeProvider = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most important job for this fn is to generate correct storageKey for theme persistenc.
|
||||||
|
* `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
|
||||||
|
* Choosing the right storageKey avoids theme flickering caused by another page using different theme
|
||||||
|
* So, we handle all the cases here namely,
|
||||||
|
* - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
|
||||||
|
* - Embeds using different namespace. They can be completely themed different on the same page.
|
||||||
|
* - Embeds using the same namespace but showing different cal.com links with different themes
|
||||||
|
* - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
|
||||||
|
* - App has different theme then Booking Pages.
|
||||||
|
*
|
||||||
|
* All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
|
||||||
|
*
|
||||||
|
* Some things to note:
|
||||||
|
* - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
|
||||||
|
* - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
|
||||||
|
* - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
|
||||||
|
* - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
|
||||||
|
* because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
|
||||||
|
* - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
|
||||||
|
* - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
|
||||||
|
* - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
|
||||||
|
* - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
|
||||||
|
* - ... and so on ...
|
||||||
|
*/
|
||||||
|
function getThemeProviderProps({
|
||||||
|
props,
|
||||||
|
isEmbedMode,
|
||||||
|
embedNamespace,
|
||||||
|
router,
|
||||||
|
}: {
|
||||||
|
props: Omit<CalcomThemeProps, "children">;
|
||||||
|
isEmbedMode: boolean;
|
||||||
|
embedNamespace: string | null;
|
||||||
|
router: NextRouter;
|
||||||
|
}) {
|
||||||
|
const isBookingPage = (() => {
|
||||||
|
if (typeof props.isBookingPage === "function") {
|
||||||
|
return props.isBookingPage({ router: router });
|
||||||
|
}
|
||||||
|
return props.isBookingPage;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const themeSupport = isBookingPage
|
||||||
|
? ThemeSupport.Booking
|
||||||
|
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||||
|
props.isThemeSupported === false
|
||||||
|
? ThemeSupport.None
|
||||||
|
: ThemeSupport.App;
|
||||||
|
|
||||||
|
const isBookingPageThemSupportRequired = themeSupport === ThemeSupport.Booking;
|
||||||
|
const themeBasis = props.themeBasis;
|
||||||
|
|
||||||
|
if ((isBookingPageThemSupportRequired || isEmbedMode) && !themeBasis) {
|
||||||
|
console.warn(
|
||||||
|
"`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
|
||||||
|
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||||
|
let embedExplicitlySetThemeSuffix = "";
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const embedTheme = window.getEmbedTheme();
|
||||||
|
if (embedTheme) {
|
||||||
|
embedExplicitlySetThemeSuffix = ":" + embedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageKey = isEmbedMode
|
||||||
|
? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
|
||||||
|
// Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
|
||||||
|
`embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
|
||||||
|
: themeSupport === ThemeSupport.App
|
||||||
|
? "app-theme"
|
||||||
|
: isBookingPageThemSupportRequired
|
||||||
|
? `booking-theme${appearanceIdSuffix}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
storageKey,
|
||||||
|
forcedTheme,
|
||||||
|
themeSupport,
|
||||||
|
nonce: props.nonce,
|
||||||
|
enableColorScheme: false,
|
||||||
|
enableSystem: themeSupport !== ThemeSupport.None,
|
||||||
|
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||||
|
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||||
|
key: storageKey,
|
||||||
|
attribute: "class",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const flags = useFlags();
|
const flags = useFlags();
|
||||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
@@ -132,9 +215,12 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
<SessionProvider session={session || undefined}>
|
<SessionProvider session={session || undefined}>
|
||||||
<CustomI18nextProvider {...props}>
|
<CustomI18nextProvider {...props}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||||
<CalcomThemeProvider
|
<CalcomThemeProvider
|
||||||
|
themeBasis={props.pageProps.themeBasis}
|
||||||
nonce={props.pageProps.nonce}
|
nonce={props.pageProps.nonce}
|
||||||
isThemeSupported={props.Component.isThemeSupported}>
|
isThemeSupported={props.Component.isThemeSupported}
|
||||||
|
isBookingPage={props.Component.isBookingPage}>
|
||||||
<FeatureFlagsProvider>
|
<FeatureFlagsProvider>
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
</FeatureFlagsProvider>
|
</FeatureFlagsProvider>
|
||||||
|
|||||||
+1
-4
@@ -13,9 +13,6 @@ function getCspPolicy(nonce: string) {
|
|||||||
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||||
// Maybe see how @next-safe/middleware does it if it's supported.
|
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||||
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||||
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
|
|
||||||
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
||||||
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
||||||
@@ -36,7 +33,7 @@ function getCspPolicy(nonce: string) {
|
|||||||
} app.cal.com;
|
} app.cal.com;
|
||||||
font-src 'self';
|
font-src 'self';
|
||||||
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
||||||
connect-src 'self' ${SENTRY_ENDPOINT}
|
connect-src 'self'
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
|
|
||||||
|
|
||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
|
||||||
import type { BookingResponse } from "@lib/types/booking";
|
|
||||||
|
|
||||||
type BookingCreateBodyForMutation = Omit<BookingCreateBody, "location">;
|
|
||||||
const createBooking = async (data: BookingCreateBodyForMutation) => {
|
|
||||||
const response = await fetch.post<BookingCreateBodyForMutation, BookingResponse>("/api/book/event", data);
|
|
||||||
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default createBooking;
|
|
||||||
+13
-14
@@ -7,9 +7,16 @@ import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
|||||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||||
|
|
||||||
|
let cold = true;
|
||||||
|
|
||||||
const middleware: NextMiddleware = async (req) => {
|
const middleware: NextMiddleware = async (req) => {
|
||||||
const url = req.nextUrl;
|
const url = req.nextUrl;
|
||||||
|
const requestHeaders = new Headers(req.headers);
|
||||||
|
|
||||||
|
// This console.log is required to create a report in axios for hot and cold requests
|
||||||
|
console.log(cold ? "Cold Start" : "Hot Start");
|
||||||
|
requestHeaders.set("x-cal-cold-start", cold ? "true" : "false");
|
||||||
|
cold = false;
|
||||||
if (!url.pathname.startsWith("/api")) {
|
if (!url.pathname.startsWith("/api")) {
|
||||||
//
|
//
|
||||||
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
||||||
@@ -61,27 +68,19 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api/trpc/")) {
|
if (url.pathname.startsWith("/api/trpc/")) {
|
||||||
const requestHeaders = new Headers(req.headers);
|
|
||||||
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
|
requestHeaders.set("x-cal-timezone", req.headers.get("x-vercel-ip-timezone") ?? "");
|
||||||
return NextResponse.next({
|
|
||||||
request: {
|
|
||||||
headers: requestHeaders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/auth/login")) {
|
if (url.pathname.startsWith("/auth/login")) {
|
||||||
const moreHeaders = new Headers(req.headers);
|
|
||||||
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
|
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
|
||||||
moreHeaders.set("x-csp-enforce", "true");
|
requestHeaders.set("x-csp-enforce", "true");
|
||||||
return NextResponse.next({
|
|
||||||
request: {
|
|
||||||
headers: moreHeaders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
+29
-19
@@ -1,7 +1,7 @@
|
|||||||
require("dotenv").config({ path: "../../.env" });
|
require("dotenv").config({ path: "../../.env" });
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
const glob = require("glob");
|
||||||
|
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
@@ -67,6 +67,18 @@ if (process.env.ANALYZE === "true") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins.push(withAxiom);
|
plugins.push(withAxiom);
|
||||||
|
|
||||||
|
/** Needed to rewrite public booking page, gets all static pages but [user] */
|
||||||
|
const pages = glob
|
||||||
|
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname })
|
||||||
|
.map((filename) =>
|
||||||
|
filename
|
||||||
|
.substr(6)
|
||||||
|
.replace(/(\.tsx|\.js|\.ts)/, "")
|
||||||
|
.replace(/\/.*/, "")
|
||||||
|
)
|
||||||
|
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]"));
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
i18n,
|
i18n,
|
||||||
@@ -193,6 +205,16 @@ const nextConfig = {
|
|||||||
source: "/embed/embed.js",
|
source: "/embed/embed.js",
|
||||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||||
}, */
|
}, */
|
||||||
|
{
|
||||||
|
source: `/:user((?!${pages.join("|")}).*)/:type`,
|
||||||
|
destination: "/new-booker/:user/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/team/:slug/:type",
|
||||||
|
destination: "/new-booker/team/:slug/:type",
|
||||||
|
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
@@ -292,6 +314,11 @@ const nextConfig = {
|
|||||||
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
|
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/support",
|
||||||
|
destination: "/event-types?openIntercom=true",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||||
@@ -318,21 +345,4 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sentryWebpackPluginOptions = {
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
silent: true, // Suppresses all logs
|
|
||||||
};
|
|
||||||
|
|
||||||
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
|
||||||
nextConfig.sentry = {
|
|
||||||
hideSourceMaps: true,
|
|
||||||
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
|
|
||||||
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sentry should be the last thing to export to catch everything right
|
|
||||||
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
|
|
||||||
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
|
|
||||||
: moduleExports;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.8.0",
|
"version": "2.9.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"@daily-co/daily-js": "^0.37.0",
|
"@daily-co/daily-js": "^0.37.0",
|
||||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||||
"@glidejs/glide": "^3.5.2",
|
"@glidejs/glide": "^3.5.2",
|
||||||
"@heroicons/react": "^1.0.6",
|
|
||||||
"@hookform/error-message": "^2.0.0",
|
"@hookform/error-message": "^2.0.0",
|
||||||
"@hookform/resolvers": "^2.9.7",
|
"@hookform/resolvers": "^2.9.7",
|
||||||
"@next-auth/prisma-adapter": "^1.0.4",
|
"@next-auth/prisma-adapter": "^1.0.4",
|
||||||
@@ -57,7 +56,6 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"@sentry/nextjs": "^7.20.0",
|
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
@@ -116,14 +114,11 @@
|
|||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
"react-timezone-select": "^1.4.0",
|
"react-timezone-select": "^1.4.0",
|
||||||
"react-use-intercom": "1.5.1",
|
"react-use-intercom": "1.5.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"remove-markdown": "^0.5.0",
|
||||||
"react-window": "^1.8.7",
|
|
||||||
"remark": "^14.0.2",
|
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
"sanitize-html": "^2.10.0",
|
"sanitize-html": "^2.10.0",
|
||||||
"schema-dts": "^1.1.0",
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"strip-markdown": "^5.0.0",
|
|
||||||
"stripe": "^9.16.0",
|
"stripe": "^9.16.0",
|
||||||
"superjson": "1.9.1",
|
"superjson": "1.9.1",
|
||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
@@ -156,8 +151,7 @@
|
|||||||
"@types/qrcode": "^1.4.3",
|
"@types/qrcode": "^1.4.3",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-phone-number-input": "^3.0.14",
|
"@types/react-phone-number-input": "^3.0.14",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/remove-markdown": "^0.3.1",
|
||||||
"@types/react-window": "^1.8.5",
|
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/stripe": "^8.0.417",
|
"@types/stripe": "^8.0.417",
|
||||||
"@types/uuid": "8.3.1",
|
"@types/uuid": "8.3.1",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
|
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function Custom404() {
|
||||||
@@ -366,6 +368,8 @@ export default function Custom404() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Custom404.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||||
const ssr = await ssgInit(context);
|
const ssr = await ssgInit(context);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
import { Copy } from "@calcom/ui/components/icon";
|
import { Copy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function Error500() {
|
export default function Error500() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -52,3 +54,5 @@ export default function Error500() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Error500.PageWrapper = PageWrapper;
|
||||||
+34
-17
@@ -1,9 +1,7 @@
|
|||||||
import { BadgeCheckIcon } from "@heroicons/react/solid";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -24,20 +22,31 @@ import defaultEvents, {
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||||
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
const {
|
||||||
|
users,
|
||||||
|
profile,
|
||||||
|
eventTypes,
|
||||||
|
isDynamicGroup,
|
||||||
|
dynamicNames,
|
||||||
|
dynamicUsernames,
|
||||||
|
isSingleUser,
|
||||||
|
markdownStrippedBio,
|
||||||
|
} = props;
|
||||||
const [user] = users; //To be used when we only have a single user, not dynamic group
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(user.theme);
|
useTheme(user.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -59,7 +68,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{eventTypes.map((type, index) => (
|
{eventTypes.map((type, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
className=" border-subtle bg-default dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||||
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
<Link
|
<Link
|
||||||
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||||
@@ -93,24 +102,23 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
const query = { ...router.query };
|
const query = { ...router.query };
|
||||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const telemetry = useTelemetry();
|
|
||||||
|
|
||||||
useEffect(() => {
|
/*
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
||||||
}
|
}
|
||||||
}, [telemetry, router.asPath]);
|
}, [telemetry, router.asPath]); */
|
||||||
const isEventListEmpty = eventTypes.length === 0;
|
const isEventListEmpty = eventTypes.length === 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
title={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
||||||
description={
|
description={isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : markdownStrippedBio}
|
||||||
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
|
|
||||||
}
|
|
||||||
meeting={{
|
meeting={{
|
||||||
title: isDynamicGroup ? "" : `${user.bio}`,
|
title: isDynamicGroup ? "" : markdownStrippedBio,
|
||||||
profile: { name: `${profile.name}`, image: null },
|
profile: { name: `${profile.name}`, image: null },
|
||||||
users: isDynamicGroup
|
users: isDynamicGroup
|
||||||
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
||||||
@@ -122,7 +130,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? " border-booker border-booker-width bg-default rounded-md border" : "",
|
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
|
||||||
"max-w-3xl py-24 px-4"
|
"max-w-3xl py-24 px-4"
|
||||||
)}>
|
)}>
|
||||||
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
||||||
@@ -130,12 +138,14 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
|
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
|
||||||
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
<h1 className="font-cal text-emphasis mb-1 text-3xl">
|
||||||
{nameOrUsername}
|
{nameOrUsername}
|
||||||
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
|
{user.verified && (
|
||||||
|
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
{!isBioEmpty && (
|
{!isBioEmpty && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" text-subtle text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -160,7 +170,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<div
|
<div
|
||||||
key={type.id}
|
key={type.id}
|
||||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||||
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
className="bg-default border-subtle dark:bg-muted dark:hover:bg-emphasis hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||||
<ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
<ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||||
<div className="block w-full p-5">
|
<div className="block w-full p-5">
|
||||||
@@ -196,6 +206,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User.isBookingPage = true;
|
||||||
|
User.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||||
return (
|
return (
|
||||||
await prisma.eventType.findMany({
|
await prisma.eventType.findMany({
|
||||||
@@ -338,11 +351,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||||
|
|
||||||
|
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
users,
|
users,
|
||||||
safeBio,
|
safeBio,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: isDynamicGroup ? null : user.username,
|
||||||
user: {
|
user: {
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
},
|
},
|
||||||
@@ -357,6 +373,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
dynamicNames,
|
dynamicNames,
|
||||||
dynamicUsernames,
|
dynamicUsernames,
|
||||||
isSingleUser,
|
isSingleUser,
|
||||||
|
markdownStrippedBio,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -12,6 +12,7 @@ import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
||||||
@@ -54,6 +55,9 @@ export default function Type(props: AvailabilityPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type.isBookingPage = true;
|
||||||
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
@@ -173,6 +177,8 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
slug: `${user.username}/${eventType.slug}`,
|
slug: `${user.username}/${eventType.slug}`,
|
||||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
},
|
},
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: user.username,
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
@@ -303,6 +309,8 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
props: {
|
props: {
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
away: false,
|
away: false,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssg.dehydrate(),
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
|||||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||||
import { getAppFromSlug } from "@calcom/app-store/utils";
|
import { getAppFromSlug } from "@calcom/app-store/utils";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import getBooking from "@calcom/features/bookings/lib/get-booking";
|
||||||
|
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import {
|
import {
|
||||||
@@ -13,8 +15,6 @@ import {
|
|||||||
getGroupName,
|
getGroupName,
|
||||||
getUsernameList,
|
getUsernameList,
|
||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
import getBooking from "@calcom/lib/getBooking";
|
|
||||||
import type { GetBookingType } from "@calcom/lib/getBooking";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -69,6 +70,9 @@ export default function Book(props: BookPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Book.isBookingPage = true;
|
||||||
|
Book.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
bookingUid: z.string().optional(),
|
bookingUid: z.string().optional(),
|
||||||
count: z.coerce.number().optional(),
|
count: z.coerce.number().optional(),
|
||||||
@@ -289,6 +293,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
props: {
|
props: {
|
||||||
away: user.away,
|
away: user.away,
|
||||||
profile,
|
profile,
|
||||||
|
// Dynamic group has no theme preference right now. It uses system theme.
|
||||||
|
themeBasis: isDynamicGroupBooking ? null : user.username,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
currentSlotBooking: currentSlotBooking,
|
currentSlotBooking: currentSlotBooking,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const getStaticProps: GetStaticProps<
|
|||||||
{ user: string }
|
{ user: string }
|
||||||
> = async (context) => {
|
> = async (context) => {
|
||||||
const { user: username, month } = paramsSchema.parse(context.params);
|
const { user: username, month } = paramsSchema.parse(context.params);
|
||||||
const user = await prisma.user.findUnique({
|
const userWithCredentials = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
},
|
},
|
||||||
@@ -34,14 +34,15 @@ export const getStaticProps: GetStaticProps<
|
|||||||
).startOf("day");
|
).startOf("day");
|
||||||
const endDate = startDate.endOf("month");
|
const endDate = startDate.endOf("month");
|
||||||
try {
|
try {
|
||||||
const results = user?.credentials
|
const results = userWithCredentials?.credentials
|
||||||
? await getCachedResults(
|
? await getCachedResults(
|
||||||
user?.credentials,
|
userWithCredentials?.credentials,
|
||||||
startDate.format(),
|
startDate.format(),
|
||||||
endDate.format(),
|
endDate.format(),
|
||||||
user?.selectedCalendars
|
userWithCredentials?.selectedCalendars
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { results, date: new Date().toISOString() },
|
props: { results, date: new Date().toISOString() },
|
||||||
revalidate: 1,
|
revalidate: 1,
|
||||||
|
|||||||
+3
-91
@@ -1,101 +1,13 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import localFont from "next/font/local";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
|
||||||
import { useEmbedUiConfig } from "@calcom/embed-core/src/embed-iframe";
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
import AppProviders from "@lib/app-providers";
|
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
|
||||||
|
|
||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
|
||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
|
||||||
const calFont = localFont({
|
|
||||||
src: "../fonts/CalSans-SemiBold.woff2",
|
|
||||||
variable: "--font-cal",
|
|
||||||
preload: true,
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
function MyApp(props: AppProps) {
|
function MyApp(props: AppProps) {
|
||||||
const { Component, pageProps, err, router } = props;
|
const { Component, pageProps } = props;
|
||||||
let pageStatus = "200";
|
if (Component.PageWrapper !== undefined) return Component.PageWrapper(props);
|
||||||
const { cssVarsPerTheme } = useEmbedUiConfig();
|
return <Component {...pageProps} />;
|
||||||
const cssVarsStyle = [];
|
|
||||||
if (cssVarsPerTheme) {
|
|
||||||
for (const [themeName, cssVars] of Object.entries(cssVarsPerTheme)) {
|
|
||||||
cssVarsStyle.push(`.${themeName} {`);
|
|
||||||
for (const [cssVarName, value] of Object.entries(cssVars)) {
|
|
||||||
cssVarsStyle.push(`--${cssVarName}: ${value};`);
|
|
||||||
}
|
|
||||||
cssVarsStyle.push(`}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (router.pathname === "/404") {
|
|
||||||
pageStatus = "404";
|
|
||||||
} else if (router.pathname === "/500") {
|
|
||||||
pageStatus = "500";
|
|
||||||
}
|
|
||||||
|
|
||||||
// On client side don't let nonce creep into DOM
|
|
||||||
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
|
||||||
// See https://github.com/kentcdodds/nonce-hydration-issues
|
|
||||||
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
|
||||||
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
|
||||||
const providerProps = {
|
|
||||||
...props,
|
|
||||||
pageProps: {
|
|
||||||
...props.pageProps,
|
|
||||||
nonce,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Use the layout defined at the page level, if available
|
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
|
||||||
return (
|
|
||||||
<AppProviders {...providerProps}>
|
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
|
||||||
<I18nLanguageHandler />
|
|
||||||
<Script
|
|
||||||
nonce={nonce}
|
|
||||||
id="page-status"
|
|
||||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
|
||||||
/>
|
|
||||||
<style jsx global>{`
|
|
||||||
:root {
|
|
||||||
--font-inter: ${interFont.style.fontFamily};
|
|
||||||
--font-cal: ${calFont.style.fontFamily};
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
<style jsx global>
|
|
||||||
{`
|
|
||||||
${cssVarsStyle.join("")}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
||||||
</Head>
|
|
||||||
{getLayout(
|
|
||||||
Component.requiresLicense ? (
|
|
||||||
<LicenseRequired>
|
|
||||||
<Component {...pageProps} err={err} />
|
|
||||||
</LicenseRequired>
|
|
||||||
) : (
|
|
||||||
<Component {...pageProps} err={err} />
|
|
||||||
),
|
|
||||||
router
|
|
||||||
)}
|
|
||||||
</AppProviders>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default trpc.withTRPC(MyApp);
|
export default trpc.withTRPC(MyApp);
|
||||||
@@ -1,23 +1,29 @@
|
|||||||
|
import type { NextPageContext } from "next";
|
||||||
import type { DocumentContext, DocumentProps } from "next/document";
|
import type { DocumentContext, DocumentProps } from "next/document";
|
||||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||||
import Script from "next/script";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
|
||||||
|
|
||||||
import { csp } from "@lib/csp";
|
import { csp } from "@lib/csp";
|
||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
function setHeader(ctx: NextPageContext, name: string, value: string) {
|
||||||
|
try {
|
||||||
|
ctx.res?.setHeader(name, value);
|
||||||
|
} catch (e) {
|
||||||
|
// Getting "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client" when revalidate calendar chache
|
||||||
|
console.log(`Error setting header ${name}=${value} for ${ctx.asPath || "unknown asPath"}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
class MyDocument extends Document<Props> {
|
class MyDocument extends Document<Props> {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
||||||
if (!process.env.CSP_POLICY) {
|
if (!process.env.CSP_POLICY) {
|
||||||
ctx.res?.setHeader("x-csp", "not-opted-in");
|
setHeader(ctx, "x-csp", "not-opted-in");
|
||||||
} else if (!ctx.res?.getHeader("x-csp")) {
|
} else if (!ctx.res?.getHeader("x-csp")) {
|
||||||
// If x-csp not set by gSSP, then it's initialPropsOnly
|
// If x-csp not set by gSSP, then it's initialPropsOnly
|
||||||
ctx.res?.setHeader("x-csp", "initialPropsOnly");
|
setHeader(ctx, "x-csp", "initialPropsOnly");
|
||||||
}
|
}
|
||||||
|
|
||||||
const asPath = ctx.asPath || "";
|
const asPath = ctx.asPath || "";
|
||||||
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
|
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
|
||||||
const parsedUrl = new URL(asPath, "https://dummyurl");
|
const parsedUrl = new URL(asPath, "https://dummyurl");
|
||||||
@@ -31,9 +37,8 @@ class MyDocument extends Document<Props> {
|
|||||||
const { isEmbed } = this.props;
|
const { isEmbed } = this.props;
|
||||||
const nonceParsed = z.string().safeParse(this.props.nonce);
|
const nonceParsed = z.string().safeParse(this.props.nonce);
|
||||||
const nonce = nonceParsed.success ? nonceParsed.data : "";
|
const nonce = nonceParsed.success ? nonceParsed.data : "";
|
||||||
const dir = getDirFromLang(locale);
|
|
||||||
return (
|
return (
|
||||||
<Html lang={locale} dir={dir}>
|
<Html lang={locale}>
|
||||||
<Head nonce={nonce}>
|
<Head nonce={nonce}>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
@@ -41,8 +46,8 @@ class MyDocument extends Document<Props> {
|
|||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||||
<meta name="theme-color" content="var(--cal-bg)" />
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
|
||||||
<Script src="/embed-init-iframe.js" strategy="beforeInteractive" />
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -2,17 +2,15 @@
|
|||||||
* Typescript class based component for custom-error
|
* Typescript class based component for custom-error
|
||||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||||
*/
|
*/
|
||||||
import * as Sentry from "@sentry/nextjs";
|
|
||||||
import type { NextPage, NextPageContext } from "next";
|
import type { NextPage, NextPageContext } from "next";
|
||||||
import type { ErrorProps } from "next/error";
|
import type { ErrorProps } from "next/error";
|
||||||
import NextError from "next/error";
|
import NextError from "next/error";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
import { ErrorPage } from "@components/error/error-page";
|
import { ErrorPage } from "@components/error/error-page";
|
||||||
|
|
||||||
// Adds HttpException to the list of possible error types.
|
// Adds HttpException to the list of possible error types.
|
||||||
@@ -50,7 +48,6 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
|
|||||||
*/
|
*/
|
||||||
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
||||||
const { res, err, asPath } = ctx;
|
const { res, err, asPath } = ctx;
|
||||||
await Sentry.captureUnderscoreErrorException(ctx);
|
|
||||||
const errorInitialProps = (await NextError.getInitialProps({
|
const errorInitialProps = (await NextError.getInitialProps({
|
||||||
res,
|
res,
|
||||||
err,
|
err,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
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 { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
// This is the callback endpoint for the OIDC provider
|
// This is the callback endpoint for the OIDC provider
|
||||||
// A team must set this endpoint in the OIDC provider's configuration
|
// A team must set this endpoint in the OIDC provider's configuration
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { OAuthReq } from "@boxyhq/saml-jackson";
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
import type { HttpError } from "@calcom/lib/http-error";
|
||||||
import type { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const { oauthController } = await jackson();
|
const { oauthController } = await jackson();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
@@ -8,6 +7,7 @@ import { HttpError } from "@calcom/lib/http-error";
|
|||||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
username: z
|
username: z
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { IdentityProvider } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
@@ -36,14 +36,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is actually an existingUser if username matches
|
// There is an existingUser if the username matches
|
||||||
// OR if email matches and both username and password are set
|
// OR if the email matches AND either the email is verified
|
||||||
|
// or both username and password are set
|
||||||
const existingUser = await prisma.user.findFirst({
|
const existingUser = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
{ username },
|
{ 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 type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { authenticator } from "otplib";
|
import { authenticator } from "otplib";
|
||||||
import qrcode from "qrcode";
|
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 { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const userWithCredentials = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: session.user.id,
|
id: session.user.id,
|
||||||
},
|
},
|
||||||
@@ -25,11 +25,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
selectedCalendars: true,
|
selectedCalendars: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!userWithCredentials) {
|
||||||
if (!user) {
|
|
||||||
res.status(401).json({ message: "Not authenticated" });
|
res.status(401).json({ message: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { credentials, ...user } = userWithCredentials;
|
||||||
|
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
await prisma.selectedCalendar.upsert({
|
await prisma.selectedCalendar.upsert({
|
||||||
@@ -80,7 +80,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
});
|
});
|
||||||
|
|
||||||
// get user's credentials + their connected integrations
|
// get user's credentials + their connected integrations
|
||||||
const calendarCredentials = getCalendarCredentials(user.credentials);
|
const calendarCredentials = getCalendarCredentials(credentials);
|
||||||
// get all the connected integrations' calendars (from third party)
|
// get all the connected integrations' calendars (from third party)
|
||||||
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
|
||||||
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
|
const calendars = connectedCalendars.flatMap((c) => c.calendars).filter(notEmpty);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BookingStatus, ReminderType } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
@@ -7,6 +6,7 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR
|
|||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import type { Session } from "next-auth";
|
|||||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
const defaultIntegrationAddHandler = async ({
|
const defaultIntegrationAddHandler = async ({
|
||||||
slug,
|
slug,
|
||||||
supportsMultipleInstalls,
|
supportsMultipleInstalls,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const session = await getServerSession({ req, res });
|
||||||
|
const secret = process.env.INTERCOM_SECRET;
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return res.status(401).json({ message: "user not authenticated" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
return res.status(400).json({ message: "Intercom Identity Verification secret not set" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmac = crypto.createHmac("sha256", secret);
|
||||||
|
hmac.update(String(session.user.id));
|
||||||
|
const hash = hmac.digest("hex");
|
||||||
|
|
||||||
|
return res.status(200).json({ hash });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: handler }),
|
||||||
|
});
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { UserPermissionRole } from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -7,7 +6,7 @@ import { defaultResponder } from "@calcom/lib/server";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer/_router";
|
||||||
|
|
||||||
enum DirectAction {
|
enum DirectAction {
|
||||||
ACCEPT = "accept",
|
ACCEPT = "accept",
|
||||||
@@ -27,31 +26,18 @@ const decryptedSchema = z.object({
|
|||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
const { action, token, reason } = querySchema.parse(req.query);
|
const { action, token, reason } = querySchema.parse(req.query);
|
||||||
const { bookingUid, userId } = decryptedSchema.parse(
|
const { bookingUid } = decryptedSchema.parse(
|
||||||
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||||
);
|
);
|
||||||
|
|
||||||
/** We shape the session as required by tRPC router */
|
|
||||||
async function sessionGetter() {
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: userId,
|
|
||||||
username: "" /* Not used in this context */,
|
|
||||||
role: UserPermissionRole.USER,
|
|
||||||
},
|
|
||||||
hasValidLicense: true,
|
|
||||||
expires: "" /* Not used in this context */,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const booking = await prisma.booking.findUniqueOrThrow({
|
const booking = await prisma.booking.findUniqueOrThrow({
|
||||||
where: { uid: bookingUid },
|
where: { uid: bookingUid },
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @see https://trpc.io/docs/server-side-calls */
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
const ctx = await createContext({ req, res }, sessionGetter);
|
const ctx = await createContext({ req, res });
|
||||||
const caller = viewerRouter.createCaller(ctx);
|
const caller = viewerRouter.createCaller({ ...ctx, req, res });
|
||||||
await caller.bookings.confirm({
|
await caller.bookings.confirm({
|
||||||
bookingId: booking.id,
|
bookingId: booking.id,
|
||||||
recurringEventId: booking.recurringEventId || undefined,
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
|
|||||||
Loaded 100 of 1091 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user