Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e8c96c267 | ||
|
|
fab765c5ee | ||
|
|
89ed07a097 | ||
|
|
d7d73d2caf | ||
|
|
aa92730e28 | ||
|
|
430430e0cd | ||
|
|
62713940b0 | ||
|
|
75aef09338 | ||
|
|
4e65c30e18 | ||
|
|
8b873e2828 | ||
|
|
e70d179628 | ||
|
|
ca9a836300 | ||
|
|
abdf72dc0f | ||
|
|
227cd165f6 | ||
|
|
a0135cdc59 | ||
|
|
112d504148 | ||
|
|
d275930732 | ||
|
|
97e33bdd7e | ||
|
|
3f13791faf | ||
|
|
359cfc4be0 | ||
|
|
1ad6bba0ab | ||
|
|
6dc4211d81 | ||
|
|
e5f0452728 | ||
|
|
d95bba63f4 | ||
|
|
6f48869021 | ||
|
|
845bfae887 | ||
|
|
3fe02fba26 | ||
|
|
4ca5ca9fe2 | ||
|
|
364e187080 | ||
|
|
b1193eef1e | ||
|
|
e110f12cf1 | ||
|
|
022b513935 | ||
|
|
010f39a7a4 | ||
|
|
6b6eed66eb | ||
|
|
c214e01f03 | ||
|
|
a28ac3634d | ||
|
|
a0709fac88 | ||
|
|
e5b6a3332e | ||
|
|
979686920a | ||
|
|
793850f3aa | ||
|
|
fa9d72fc0a | ||
|
|
0d485ffed6 | ||
|
|
9a2e55d528 | ||
|
|
81ecbb86a5 | ||
|
|
2cd816932b | ||
|
|
d83411162d | ||
|
|
c9c54db3a1 | ||
|
|
d100600789 | ||
|
|
6f9011a167 | ||
|
|
ae004c8a6c | ||
|
|
ccbb16fe76 | ||
|
|
b77ce4a010 | ||
|
|
f72f709e34 | ||
|
|
37ceb25f8c | ||
|
|
0141d8f954 | ||
|
|
47fe791006 | ||
|
|
0cbaba73e4 | ||
|
|
aaac491dc7 | ||
|
|
fc4203e963 | ||
|
|
8430e9612e | ||
|
|
de81934619 | ||
|
|
5aedcbee55 | ||
|
|
7bc8d7e991 | ||
|
|
df183f5882 | ||
|
|
27d9fad292 | ||
|
|
bed798d440 | ||
|
|
e5a8a4e34d | ||
|
|
de446db434 | ||
|
|
81546412bf | ||
|
|
459530fba3 | ||
|
|
42766b42c0 | ||
|
|
38b3563350 | ||
|
|
37159914cf | ||
|
|
268bf68532 | ||
|
|
b02c7e01d5 | ||
|
|
45d10a3bc4 | ||
|
|
9250b20c1d | ||
|
|
d9da311141 | ||
|
|
89a52dad21 | ||
|
|
2a1acc1294 | ||
|
|
8302990f84 | ||
|
|
5a03b61d63 | ||
|
|
6b6f239936 | ||
|
|
9c2748e8e9 | ||
|
|
bb5cc33737 | ||
|
|
5616a64827 | ||
|
|
33530ad8c8 | ||
|
|
fe14839f1b | ||
|
|
15123c1aa5 | ||
|
|
71ee9c72fe | ||
|
|
5503539856 | ||
|
|
6548f01efa | ||
|
|
518a4484d6 | ||
|
|
3535a25a59 | ||
|
|
06cc6741e6 | ||
|
|
ed1478e1ba | ||
|
|
854c05d673 | ||
|
|
bcf5fb18c5 | ||
|
|
3f822e814b | ||
|
|
4fd25c7215 | ||
|
|
f42bed2a55 | ||
|
|
469c205033 | ||
|
|
31551f3b9e | ||
|
|
69925af17c | ||
|
|
2502868ebc | ||
|
|
38d242add9 | ||
|
|
c272e281cf | ||
|
|
d4e19b0fa4 | ||
|
|
c12877a58f | ||
|
|
af83350d04 | ||
|
|
9d0cf6be0b | ||
|
|
2fe9c782d4 | ||
|
|
d0a3d8d144 | ||
|
|
cefcb1eaf8 | ||
|
|
83b60de640 | ||
|
|
a076418989 | ||
|
|
64ecce1ccb | ||
|
|
cd56a5d4f0 | ||
|
|
d5ca121ac6 | ||
|
|
1e20e80b65 | ||
|
|
75ac3fd40c | ||
|
|
3b91cc7f02 | ||
|
|
23450b61e0 | ||
|
|
dcc226976c | ||
|
|
196fde72f6 | ||
|
|
1e913cd406 | ||
|
|
02425702d1 | ||
|
|
2494655369 | ||
|
|
0e0a78c47e | ||
|
|
ed02c10f54 | ||
|
|
9c40f76184 | ||
|
|
8eda30d88c | ||
|
|
9de19d35a8 | ||
|
|
0770403ca5 | ||
|
|
1a45cb35c1 | ||
|
|
15cdff76d9 | ||
|
|
8c4eb4b5fe | ||
|
|
ae30aafafe | ||
|
|
3b74717591 | ||
|
|
7a6349b4c0 | ||
|
|
3b51fff88d | ||
|
+5 |
2a0b0be9d0 | ||
|
|
f373c9d818 | ||
|
|
540bf3a1cb | ||
|
|
2d9064e92f | ||
|
|
9487819ec1 | ||
|
|
49bf14b196 | ||
|
|
344f16d6cc | ||
|
|
ccbf3b1e54 | ||
|
|
dcf0fad35d | ||
|
|
a836800c69 | ||
|
|
b8c6ac43f7 | ||
|
|
f5e0c6b384 | ||
|
|
2a5dd9035f | ||
|
|
2cddad4336 | ||
|
|
4bcbff51b1 | ||
|
|
ae541e4c37 | ||
|
|
f254711d85 | ||
|
|
533e208643 | ||
|
|
795574ef88 | ||
|
|
48611d5c99 | ||
|
|
11a07961d0 | ||
|
|
6be0fd52e7 | ||
|
|
78da7ba338 | ||
|
|
9a72c648c5 | ||
|
|
3e42da7fc7 | ||
|
|
1d986cad04 | ||
|
|
f603e4eff2 | ||
|
|
1f4e6c1987 | ||
|
|
e57f734e79 | ||
|
|
7fad76ae23 | ||
|
|
7659a016b4 | ||
|
|
34fe1c4a95 | ||
|
|
205f73c4bc | ||
|
|
c4bd2167e1 | ||
|
|
49a7111b66 | ||
|
|
ca045a040b | ||
|
|
a8b4198bf7 | ||
|
|
2ab24e530b | ||
|
|
734b06dfdf | ||
|
|
467084993b | ||
|
|
1e2c8516d2 | ||
|
|
d57738f325 | ||
|
|
c39a7dba85 | ||
|
|
fd37397e5c | ||
|
|
df0a522656 | ||
|
|
916468b8cc | ||
|
|
b789e58624 | ||
|
|
4368536141 | ||
|
|
9eef9766ce | ||
|
|
68dbfbd7d0 | ||
|
|
692a84a86e | ||
|
|
1671095c94 | ||
|
|
3b896bee5c | ||
|
|
cc1985442e | ||
|
|
630f438b32 | ||
|
|
6fae01d45a | ||
|
|
7009373b11 | ||
|
|
c2f150dbab | ||
|
|
688541923b | ||
|
|
20402ac26c | ||
|
|
a54952b41a | ||
|
|
987756c4d4 | ||
|
|
3ee294ffc5 | ||
|
|
a638708871 | ||
|
|
3456cc5521 | ||
|
|
546f0c3881 | ||
|
|
7b18272d60 | ||
|
|
cf6efabdbb | ||
|
|
92d15a00f5 | ||
|
|
2f2b72dd54 | ||
|
|
8fd5d6b5b5 | ||
|
|
2078f12a85 | ||
|
|
77b8eb8a7f | ||
|
|
463787b607 | ||
|
|
1f4e7ca695 | ||
|
|
9eb52bbfb2 | ||
|
|
61e6f76bc6 | ||
|
|
2e6c3a6068 | ||
|
|
8f48ecafec | ||
|
|
8849d1148c | ||
|
|
98a4684a2b | ||
|
|
da0bdfacae | ||
|
|
68c7425922 | ||
|
|
c3418cbe1f | ||
|
|
3a641e0bc1 | ||
|
|
b7fcf6ade1 | ||
|
|
53d160e495 | ||
|
|
df4df6846b | ||
|
|
c1bbb9b139 | ||
|
|
56c07ec351 | ||
|
|
3080b47aec | ||
|
|
c1e23bfdc6 | ||
|
|
e7ff5508d7 | ||
|
|
5850d227ee | ||
|
|
ede987ba19 | ||
|
|
bc0d83164c | ||
|
|
30f0b080c5 | ||
|
|
ee39ea316f | ||
|
|
fbc2708259 | ||
|
|
9bb83aba7e | ||
|
|
3a67e5617e | ||
|
|
f401aa7669 | ||
|
|
a6e0678e59 | ||
|
|
e832015f26 | ||
|
|
5e86881e15 | ||
|
|
3b71f98460 | ||
|
|
144d57adb2 | ||
|
|
e1f1c15318 | ||
|
|
d88b1b2f8f | ||
|
|
2e61c15f2c | ||
|
|
df73473c7f | ||
|
|
7167973afd | ||
|
|
3c17bc2d1a | ||
|
|
a9a295dc54 | ||
|
+2 |
21dd1f4e95 |
No files matched your search
@@ -0,0 +1,42 @@
|
|||||||
|
// 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"
|
||||||
|
}
|
||||||
+7
-3
@@ -7,14 +7,13 @@
|
|||||||
# - E-MAIL SETTINGS
|
# - E-MAIL SETTINGS
|
||||||
|
|
||||||
# - LICENSE *************************************************************************************************
|
# - LICENSE *************************************************************************************************
|
||||||
# Set this value to 'agree' to accept our license:
|
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||||
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
|
|
||||||
#
|
#
|
||||||
# Summary of terms:
|
# Summary of terms:
|
||||||
# - The codebase has to stay open source, whether it was modified or not
|
# - The codebase has to stay open source, whether it was modified or not
|
||||||
# - You can not repackage or sell the codebase
|
# - You can not repackage or sell the codebase
|
||||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||||
NEXT_PUBLIC_LICENSE_CONSENT=''
|
#
|
||||||
# To enable enterprise-only features, fill your license key in here.
|
# To enable enterprise-only features, fill your license key in here.
|
||||||
# @see https://console.cal.com
|
# @see https://console.cal.com
|
||||||
CALCOM_LICENSE_KEY=
|
CALCOM_LICENSE_KEY=
|
||||||
@@ -86,6 +85,9 @@ SENDGRID_EMAIL=
|
|||||||
TWILIO_SID=
|
TWILIO_SID=
|
||||||
TWILIO_TOKEN=
|
TWILIO_TOKEN=
|
||||||
TWILIO_MESSAGING_SID=
|
TWILIO_MESSAGING_SID=
|
||||||
|
TWILIO_PHONE_NUMBER=
|
||||||
|
NEXT_PUBLIC_SENDER_ID=
|
||||||
|
TWILIO_VERIFY_SID=
|
||||||
|
|
||||||
# This is used so we can bypass emails in auth flows for E2E testing
|
# This is used so we can bypass emails in auth flows for E2E testing
|
||||||
# Set it to "1" if you need to run E2E tests locally
|
# Set it to "1" if you need to run E2E tests locally
|
||||||
@@ -100,6 +102,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
|
|||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_CLIENT_ID=
|
STRIPE_CLIENT_ID=
|
||||||
|
PAYMENT_FEE_FIXED=
|
||||||
|
PAYMENT_FEE_PERCENTAGE=
|
||||||
|
|
||||||
# Use for internal Public API Keys and optional
|
# Use for internal Public API Keys and optional
|
||||||
API_KEY_PREFIX=cal_
|
API_KEY_PREFIX=cal_
|
||||||
|
|||||||
@@ -3,3 +3,10 @@
|
|||||||
|
|
||||||
automerge:
|
automerge:
|
||||||
- apps/web/public/static/locales/**/*.json
|
- apps/web/public/static/locales/**/*.json
|
||||||
|
|
||||||
|
"❗️ migrations":
|
||||||
|
- packages/prisma/migrations/**/migration.sql
|
||||||
|
|
||||||
|
"❗️ .env changes":
|
||||||
|
- .env.example
|
||||||
|
- .env.appStore.example
|
||||||
@@ -22,6 +22,7 @@ jobs:
|
|||||||
upload_translations: true
|
upload_translations: true
|
||||||
download_translations: true
|
download_translations: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
# So required checks run on Crowdin PRs
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
@@ -12,8 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v4
|
||||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
|
||||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
|
||||||
if: ${{ github.actor == 'github-actions[bot]' }}
|
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||||
|
sync-labels: ""
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<!-- PROJECT LOGO -->
|
<!-- PROJECT LOGO -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/calcom/cal.com">
|
<a href="https://github.com/calcom/cal.com">
|
||||||
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
|
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
<a href="https://github.com/orgs/calcom/projects/9"><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://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
@@ -292,7 +291,11 @@ You can deploy Cal on [Railway](https://railway.app/) using the button above. Th
|
|||||||
|
|
||||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
||||||
|
|
||||||
|
### Render
|
||||||
|
|
||||||
|
[](https://render.com/deploy?repo=https://github.com/calcom/docker)
|
||||||
|
|
||||||
<!-- ROADMAP -->
|
<!-- ROADMAP -->
|
||||||
|
|
||||||
@@ -310,7 +313,18 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
|||||||
|
|
||||||
### Good First Issues
|
### Good First Issues
|
||||||
|
|
||||||
We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/25) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
We have a list of [help wanted](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) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TRANSLATIONS -->
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
Don't code but still want to contribute? Join our [slack](https://cal.com/slack) and join the [#i18n channel](https://calendso.slack.com/archives/C02BY67GMMW) and let us know what language you want to translate.
|
||||||
|
|
||||||
|
                       ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
|
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
|
||||||
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
|
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
|
||||||
|
|
||||||
|
|
||||||
### Obtaining Zoom Client ID and Secret
|
### Obtaining Zoom Client ID and Secret
|
||||||
|
|
||||||
1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account.
|
1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account.
|
||||||
@@ -419,14 +432,18 @@ following
|
|||||||
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. Create a messaging service (Develop -> Messaging -> Services)
|
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
|
||||||
6. Choose any name for the messaging service
|
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
|
||||||
7. Click 'Add Senders'
|
7. Create a messaging service (Develop -> Messaging -> Services)
|
||||||
8. Choose phone number as sender type
|
8. Choose any name for the messaging service
|
||||||
9. Add the listed phone number
|
9. Click 'Add Senders'
|
||||||
10. Leave all other fields as they are
|
10. Choose phone number as sender type
|
||||||
11. Complete setup and click ‘View my new Messaging Service’
|
11. Add the listed phone number
|
||||||
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
12. Leave all other fields as they are
|
||||||
|
13. Complete setup and click ‘View my new Messaging Service’
|
||||||
|
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||||
|
15. Create a verify service
|
||||||
|
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
||||||
|
|
||||||
<!-- LICENSE -->
|
<!-- LICENSE -->
|
||||||
|
|
||||||
|
|||||||
@@ -33,11 +33,6 @@
|
|||||||
"generator": "secret",
|
"generator": "secret",
|
||||||
"required": "true"
|
"required": "true"
|
||||||
},
|
},
|
||||||
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
|
||||||
"description": "License consent",
|
|
||||||
"value": "true",
|
|
||||||
"required": "true"
|
|
||||||
},
|
|
||||||
"CRON_API_KEY": {
|
"CRON_API_KEY": {
|
||||||
"description": "ApiKey for cronjobs",
|
"description": "ApiKey for cronjobs",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
|||||||
+1
-1
Submodule apps/api updated: 41d22c8ccb...7aebdb8c96.
+1
-1
Submodule apps/console updated: d2445da9f5...8dd0779c43.
@@ -17,7 +17,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iframe-resizer-react": "^1.1.0",
|
"iframe-resizer-react": "^1.1.0",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"nextra": "^1.1.0",
|
"nextra": "^1.1.0",
|
||||||
"nextra-theme-docs": "^1.2.2",
|
"nextra-theme-docs": "^1.2.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -14,14 +14,13 @@ Cal.com uses [TailwindCSS](https://tailwindcss.com) as a replacement for traditi
|
|||||||
|
|
||||||
CSS files should be stored in the `styles` directory within the codebase.
|
CSS files should be stored in the `styles` directory within the codebase.
|
||||||
|
|
||||||
Within the `styles` directory, you will find two CSS files, `fonts.css` and `global.css`. We suggest not to add to these files, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
|
Within the `styles` directory, you will find a single CSS file, `global.css`. We suggest not to add to this file, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
|
||||||
|
|
||||||
## Adding new stylesheets
|
## Adding new stylesheets
|
||||||
Firstly, create the CSS file inside the `styles` directory.
|
Firstly, create the CSS file inside the `styles` directory.
|
||||||
|
|
||||||
Then, open the `pages/_app.tsx` file, and you will see the following two lines:
|
Then, open the `pages/_app.tsx` file, and you will see the following two lines:
|
||||||
```javascript
|
```javascript
|
||||||
import "../styles/fonts.css";
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
|
|||||||
| ├──index.ts
|
| ├──index.ts
|
||||||
| ├──package.json
|
| ├──package.json
|
||||||
| ├──.env.example
|
| ├──.env.example
|
||||||
| ├──README.mdx
|
| ├──DESCRIPTION.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
|
|||||||
|
|
||||||
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
||||||
|
|
||||||
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead.
|
On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
|
||||||
|
|
||||||
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use.
|
The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
|
||||||
|
|
||||||
## Adding Your App to the App Store
|
## Adding Your App to the App Store
|
||||||
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
||||||
|
|||||||
@@ -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/features/**/*.stories.mdx",
|
||||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
@@ -50,6 +51,21 @@ module.exports = {
|
|||||||
vm: false,
|
vm: false,
|
||||||
zlib: false,
|
zlib: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
modules: true, // Enable modules to help you using className
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
include: path.resolve(__dirname, "../src"),
|
||||||
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -15,7 +15,11 @@ export const parameters = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
|
addDecorator((storyFn) => (
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<div style={{ margin: "2rem" }}>{storyFn()}</div>
|
||||||
|
</I18nextProvider>
|
||||||
|
));
|
||||||
|
|
||||||
window.getEmbedNamespace = () => {
|
window.getEmbedNamespace = () => {
|
||||||
const url = new URL(document.URL);
|
const url = new URL(document.URL);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"@radix-ui/react-slider": "^1.0.0",
|
"@radix-ui/react-slider": "^1.0.0",
|
||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
|
||||||
|
|
||||||
.sbdocs {
|
.sbdocs {
|
||||||
font-family: 'Inter var' !important;
|
font-family: 'Inter var' !important;
|
||||||
padding: 0!important;
|
padding: 0!important;
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
|
|
||||||
/** Docs table **/
|
/** Docs table **/
|
||||||
.custom-args-wrapper{
|
.custom-args-wrapper{
|
||||||
max-height: 600px;
|
max-height: 400px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@@ -187,3 +189,7 @@
|
|||||||
padding: 24px !important;
|
padding: 24px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.docs-story {
|
||||||
|
padding: 24px !important;
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"isarray": "2.0.5",
|
"isarray": "2.0.5",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"openapi-snippet": "^0.13.0",
|
"openapi-snippet": "^0.13.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
14.17
|
v16
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Suspense, SuspenseProps } from "react";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around `<Suspense />` which will render the `fallback` when on server
|
|
||||||
* Can be simply replaced by `<Suspense />` once React 18 is ready.
|
|
||||||
*/
|
|
||||||
export const ClientSuspense = (props: SuspenseProps) => {
|
|
||||||
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
|
|
||||||
};
|
|
||||||
@@ -44,6 +44,7 @@ type PreviewState = {
|
|||||||
palette: {
|
palette: {
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
};
|
};
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||||
|
|
||||||
@@ -125,10 +126,12 @@ const getEmbedUIInstructionString = ({
|
|||||||
apiName,
|
apiName,
|
||||||
theme,
|
theme,
|
||||||
brandColor,
|
brandColor,
|
||||||
|
hideEventTypeDetails,
|
||||||
}: {
|
}: {
|
||||||
apiName: string;
|
apiName: string;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
theme = theme !== "auto" ? theme : undefined;
|
theme = theme !== "auto" ? theme : undefined;
|
||||||
return getInstructionString({
|
return getInstructionString({
|
||||||
@@ -141,6 +144,7 @@ const getEmbedUIInstructionString = ({
|
|||||||
brandColor,
|
brandColor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hideEventTypeDetails: hideEventTypeDetails,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -253,18 +257,21 @@ const getEmbedTypeSpecificString = ({
|
|||||||
apiName: string;
|
apiName: string;
|
||||||
theme: PreviewState["theme"];
|
theme: PreviewState["theme"];
|
||||||
brandColor: string;
|
brandColor: string;
|
||||||
|
hideEventTypeDetails: boolean;
|
||||||
};
|
};
|
||||||
if (embedFramework === "react") {
|
if (embedFramework === "react") {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "cal",
|
apiName: "cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
uiInstructionStringArg = {
|
uiInstructionStringArg = {
|
||||||
apiName: "Cal",
|
apiName: "Cal",
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
brandColor: previewState.palette.brandColor,
|
brandColor: previewState.palette.brandColor,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!frameworkCodes[embedType]) {
|
if (!frameworkCodes[embedType]) {
|
||||||
@@ -632,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
|||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{embeds.map((embed, index) => (
|
{embeds.map((embed, index) => (
|
||||||
<button
|
<button
|
||||||
className="mr-2 w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100"
|
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2"
|
||||||
key={index}
|
key={index}
|
||||||
data-testid={embed.type}
|
data-testid={embed.type}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -689,6 +696,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
theme: Theme.auto,
|
theme: Theme.auto,
|
||||||
floatingPopup: {},
|
floatingPopup: {},
|
||||||
elementClick: {},
|
elementClick: {},
|
||||||
|
hideEventTypeDetails: false,
|
||||||
palette: {
|
palette: {
|
||||||
brandColor: "#000000",
|
brandColor: "#000000",
|
||||||
},
|
},
|
||||||
@@ -753,6 +761,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
name: "ui",
|
name: "ui",
|
||||||
arg: {
|
arg: {
|
||||||
theme: previewState.theme,
|
theme: previewState.theme,
|
||||||
|
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||||
styles: {
|
styles: {
|
||||||
branding: {
|
branding: {
|
||||||
...previewState.palette,
|
...previewState.palette,
|
||||||
@@ -811,7 +820,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||||
<div className="max-h-97 flex flex-col overflow-y-auto">
|
<div className="flex flex-col overflow-y-auto">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
@@ -838,7 +847,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="text-sm">Embed Window Sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center">
|
||||||
<TextField
|
<TextField
|
||||||
name="width"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
required
|
required
|
||||||
value={previewState.inline.width}
|
value={previewState.inline.width}
|
||||||
@@ -860,7 +868,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<span className="p-2">×</span>
|
<span className="p-2">×</span>
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
name="height"
|
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -888,7 +895,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<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
|
||||||
name="buttonText"
|
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setPreviewState((previewState) => {
|
setPreviewState((previewState) => {
|
||||||
@@ -908,7 +914,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-4 flex items-center justify-start",
|
"mt-4 flex items-center justify-start",
|
||||||
embedType === "floating-popup" ? "space-x-2" : "hidden"
|
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
|
||||||
)}>
|
)}>
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={true}
|
defaultChecked={true}
|
||||||
@@ -1004,6 +1010,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
|
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
|
<Switch
|
||||||
|
checked={previewState.hideEventTypeDetails}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
hideEventTypeDetails: checked,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||||
|
</div>
|
||||||
<Label className="">
|
<Label className="">
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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() {
|
||||||
@@ -21,12 +22,16 @@ export function useViewerI18n() {
|
|||||||
*/
|
*/
|
||||||
const I18nLanguageHandler = (): null => {
|
const I18nLanguageHandler = (): null => {
|
||||||
const { i18n } = useTranslation("common");
|
const { i18n } = useTranslation("common");
|
||||||
const locale = useViewerI18n().data?.locale;
|
const locale = useViewerI18n().data?.locale || "en";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (locale && i18n.language && i18n.language !== locale) {
|
if (locale && i18n.language && i18n.language !== locale) {
|
||||||
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
|
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dir = getDirFromLang(locale);
|
||||||
|
document.documentElement.setAttribute("lang", locale);
|
||||||
|
document.documentElement.setAttribute("dir", dir);
|
||||||
}, [locale, i18n]);
|
}, [locale, i18n]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
|||||||
const className = tab.className || "";
|
const className = tab.className || "";
|
||||||
return (
|
return (
|
||||||
<Component key={tab.name}>
|
<Component key={tab.name}>
|
||||||
<Link key={tab.name} href={href} {...linkProps}>
|
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
|
||||||
<a
|
<a
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
|
|||||||
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 { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
|
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui";
|
||||||
|
|
||||||
import HeadSeo from "@components/seo/head-seo";
|
|
||||||
|
|
||||||
const Component = ({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
@@ -66,7 +64,9 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
// variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
|
||||||
|
// Such apps, can only be installed once.
|
||||||
|
const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
@@ -98,8 +98,10 @@ const Component = ({
|
|||||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-sm font-medium text-gray-600">
|
<h2 className="text-sm font-medium text-gray-600">
|
||||||
<Link href={`categories/${categories[0]}`}>
|
<Link
|
||||||
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a>
|
href={`categories/${categories[0]}`}
|
||||||
|
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
||||||
|
{categories[0]}
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
• {t("published_by", { author })}
|
• {t("published_by", { author })}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
showToast,
|
showToast,
|
||||||
SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
Switch,
|
Switch,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
@@ -159,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
if (!data.connectedCalendars.length) {
|
if (!data.connectedCalendars.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.connectedCalendars.map((item) => (
|
{data.connectedCalendars.map((item) => (
|
||||||
@@ -168,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
slug={item.integration.slug}
|
slug={item.integration.slug}
|
||||||
title={item.integration.title}
|
title={item.integration.title}
|
||||||
logo={item.integration.logo}
|
logo={item.integration.logo}
|
||||||
description={item.primary?.externalId || "No external Id"}
|
description={item.primary?.email ?? item.integration.description}
|
||||||
separate={true}
|
separate={true}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-32 justify-end">
|
<div className="flex w-32 justify-end">
|
||||||
@@ -329,7 +330,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
})}
|
})}
|
||||||
description={t(`no_category_apps_description_calendar`)}
|
description={t(`no_category_apps_description_calendar`)}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button color="secondary" href="/apps/categories/calendar">
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
data-testid="connect-calendar-apps"
|
||||||
|
href="/apps/categories/calendar">
|
||||||
{t(`connect_calendar_apps`)}
|
{t(`connect_calendar_apps`)}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function IntegrationListItem(props: {
|
|||||||
expanded={!!props.children}
|
expanded={!!props.children}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
||||||
"my-0 flex-col border transition-colors duration-500 ",
|
"my-0 flex-col border transition-colors duration-500",
|
||||||
highlight ? "bg-yellow-100" : ""
|
highlight ? "bg-yellow-100" : ""
|
||||||
)}>
|
)}>
|
||||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||||
@@ -56,7 +56,7 @@ function IntegrationListItem(props: {
|
|||||||
<ListItemText component="p">{props.description}</ListItemText>
|
<ListItemText component="p">{props.description}</ListItemText>
|
||||||
{/* Alert error that key stopped working. */}
|
{/* Alert error that key stopped working. */}
|
||||||
{props.invalidCredential && (
|
{props.invalidCredential && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||||
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
{t("invalid_credential")}
|
{t("invalid_credential")}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { EmptyScreen, Icon, Shell, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui";
|
||||||
|
|
||||||
|
const tabs: HorizontalTabItemProps[] = [
|
||||||
|
{
|
||||||
|
name: "app_store",
|
||||||
|
href: "/apps",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "installed_apps",
|
||||||
|
href: "/apps/installed",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type AppsLayoutProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
actions?: (className?: string) => JSX.Element;
|
||||||
|
emptyStore?: boolean;
|
||||||
|
} & Omit<ComponentProps<typeof Shell>, "actions">;
|
||||||
|
|
||||||
|
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (session.status === "loading") return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Shell {...rest} actions={actions?.("hidden sm:block")}>
|
||||||
|
<div className="flex flex-col xl:flex-row">
|
||||||
|
<div className="block lg:hidden">
|
||||||
|
<HorizontalTabs tabs={tabs} actions={actions?.("ml-6 mr-4 mt-3 block sm:hidden")} />
|
||||||
|
</div>
|
||||||
|
<main className="w-full">
|
||||||
|
{emptyStore ? (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={Icon.FiAlertCircle}
|
||||||
|
headline={t("no_apps")}
|
||||||
|
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||||
|
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||||
|
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>{children}</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const getLayout = (page: React.ReactElement) => <AppsLayout>{page}</AppsLayout>;
|
||||||
+10
-16
@@ -1,14 +1,11 @@
|
|||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Icon, Shell } from "@calcom/ui";
|
||||||
import { Icon } from "../../../Icon";
|
import type { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem";
|
||||||
import Shell from "../Shell";
|
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
|
||||||
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
|
|
||||||
import HorizontalTabs from "../navigation/tabs/HorizontalTabs";
|
|
||||||
import type { VerticalTabItemProps } from "../navigation/tabs/VerticalTabItem";
|
|
||||||
import VerticalTabs from "../navigation/tabs/VerticalTabs";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
@@ -55,17 +52,14 @@ export default function InstalledAppsLayout({
|
|||||||
if (query.data?.items.length === 0) {
|
if (query.data?.items.length === 0) {
|
||||||
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
|
actualTabs = tabs.filter((tab) => tab.name !== InstalledAppVariants.payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest}>
|
||||||
<div className="flex flex-col p-2 md:p-0 xl:flex-row">
|
<AppCategoryNavigation
|
||||||
<div className="hidden xl:block">
|
baseURL="/apps/installed"
|
||||||
<VerticalTabs tabs={actualTabs} sticky linkProps={{ shallow: true }} />
|
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
|
||||||
</div>
|
{children}
|
||||||
<div className="block xl:hidden">
|
</AppCategoryNavigation>
|
||||||
<HorizontalTabs tabs={actualTabs} linkProps={{ shallow: true }} />
|
|
||||||
</div>
|
|
||||||
<main className="w-full xl:mx-5 xl:w-4/5 xl:pr-5">{children}</main>
|
|
||||||
</div>
|
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+4
-4
@@ -2,9 +2,9 @@ import { useSession } from "next-auth/react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { ComponentProps, useEffect } from "react";
|
import React, { ComponentProps, useEffect } from "react";
|
||||||
|
|
||||||
import ErrorBoundary from "../../../ErrorBoundary";
|
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import Shell from "../Shell";
|
import { ErrorBoundary, Shell } from "@calcom/ui";
|
||||||
import SettingsLayout from "./SettingsLayout";
|
|
||||||
import { UserPermissionRole } from ".prisma/client";
|
import { UserPermissionRole } from ".prisma/client";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
@@ -23,7 +23,7 @@ export default function AdminLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout {...rest}>
|
<SettingsLayout {...rest}>
|
||||||
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200 md:px-12">
|
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
|
||||||
<div className="flex flex-1 [&>*]:flex-1">
|
<div className="flex flex-1 [&>*]:flex-1">
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Icon, Tooltip } from "@calcom/ui";
|
import { Icon, Tooltip } from "@calcom/ui";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
return locations.length ? (
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||||
{locations.map((location) => {
|
{locations.map((location) => {
|
||||||
@@ -16,19 +19,19 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
return (
|
return (
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<Icon.FiLink className="dark:text-darkgray-600 mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 " />
|
<Icon.FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 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" : ""
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||||
)}
|
)}
|
||||||
alt={`${eventLocationType.label} icon`}
|
alt={`${eventLocationType.label} icon`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={locationKeyToString(location)}>
|
<Tooltip content={t(locationKeyToString(location) ?? "")}>
|
||||||
<p className="truncate">{locationKeyToString(location)}</p>
|
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-bookinglight font-medium">
|
<span className="text-bookinglight font-medium">
|
||||||
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
@@ -71,7 +71,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||||
{slots.length > 0 &&
|
{slots.length > 0 &&
|
||||||
slots.map((slot) => {
|
slots.map((slot) => {
|
||||||
type BookingURL = {
|
type BookingURL = {
|
||||||
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={dayjs(slot.time).format()}>
|
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||||
|
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||||
{/* Current there is no way to disable Next.js Links */}
|
{/* Current there is no way to disable Next.js Links */}
|
||||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||||
<div
|
<div
|
||||||
@@ -113,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link href={bookingUrl} prefetch={false}>
|
<Link
|
||||||
<a
|
href={bookingUrl}
|
||||||
className={classNames(
|
prefetch={false}
|
||||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
className={classNames(
|
||||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
||||||
)}
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
data-testid="time">
|
)}
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
data-testid="time">
|
||||||
{!!seatsPerTimeSlot && (
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
<p
|
{!!seatsPerTimeSlot && (
|
||||||
className={`${
|
<p
|
||||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
className={`${
|
||||||
? "text-rose-600"
|
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
? "text-rose-600"
|
||||||
? "text-yellow-500"
|
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||||
: "text-emerald-400"
|
? "text-yellow-500"
|
||||||
} text-sm`}>
|
: "text-emerald-400"
|
||||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
} text-sm`}>
|
||||||
{seatsPerTimeSlot} {t("seats_available")}
|
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||||
</p>
|
{seatsPerTimeSlot} {t("seats_available")}
|
||||||
)}
|
</p>
|
||||||
</a>
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,18 +44,17 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
const { profile, eventType, isBookingPage = false, children } = props;
|
const { profile, eventType, isBookingPage = false, children } = props;
|
||||||
const { date: bookingDate } = useRouterQuery("date");
|
const { date: bookingDate } = useRouterQuery("date");
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (eventType.metadata?.multipleDuration !== undefined) {
|
if (
|
||||||
if (!duration) {
|
eventType.metadata?.multipleDuration &&
|
||||||
setDuration(eventType.length);
|
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||||
} else {
|
) {
|
||||||
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
|
setDuration(eventType.length.toString());
|
||||||
setDuration(eventType.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||||
|
|
||||||
let requiresConfirmation = eventType?.requiresConfirmation;
|
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||||
let requiresConfirmationText = t("requires_confirmation");
|
let requiresConfirmationText = t("requires_confirmation");
|
||||||
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||||
@@ -96,7 +95,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<div>
|
<div>
|
||||||
<Icon.FiInfo
|
<Icon.FiInfo
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mr-[10px] ml-[2px] inline-block h-4 w-4",
|
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +112,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||||
)}>
|
)}>
|
||||||
<div>
|
<div>
|
||||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
</div>
|
</div>
|
||||||
{requiresConfirmationText}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,7 +127,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
)}>
|
)}>
|
||||||
<Icon.FiClock
|
<Icon.FiClock
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
|
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
isBookingPage && "mt-[2px]"
|
isBookingPage && "mt-[2px]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -22,15 +23,16 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
ActionType,
|
||||||
|
TableActions,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
|
||||||
|
|
||||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||||
|
|
||||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data.status === BookingStatus.REJECTED) {
|
if (data.status === BookingStatus.REJECTED) {
|
||||||
@@ -112,6 +115,17 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const showRecordingActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: "view_recordings",
|
||||||
|
label: t("view_recordings"),
|
||||||
|
onClick: () => {
|
||||||
|
setViewRecordingsDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
let bookedActions: ActionType[] = [
|
let bookedActions: ActionType[] = [
|
||||||
{
|
{
|
||||||
id: "cancel",
|
id: "cancel",
|
||||||
@@ -158,6 +172,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPast && isPending && !isConfirmed) {
|
||||||
|
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||||
|
}
|
||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||||
@@ -202,7 +220,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -216,7 +234,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isOpenDialog={isOpenSetLocationDialog}
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
setShowLocationModal={setIsOpenLocationDialog}
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
/>
|
/>
|
||||||
|
{showRecordingsButtons && (
|
||||||
|
<ViewRecordingsDialog
|
||||||
|
booking={booking}
|
||||||
|
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||||
|
setIsOpenDialog={setViewRecordingsDialogIsOpen}
|
||||||
|
timeFormat={user?.timeFormat ?? null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -248,7 +273,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||||
<td
|
<td
|
||||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
onClick={onClickTableData}>
|
onClick={onClickTableData}>
|
||||||
@@ -275,11 +300,6 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
{booking.eventType.team.name}
|
{booking.eventType.team.name}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
|
||||||
{t("pending_payment")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{booking.paid && (
|
{booking.paid && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{t("paid")}
|
{t("paid")}
|
||||||
@@ -343,12 +363,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
||||||
{!!booking?.eventType?.price && !booking.paid && (
|
{!!booking?.eventType?.price && !booking.paid && (
|
||||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
|
||||||
|
{t("pending_payment")}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -376,6 +398,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
|
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
<div className="hidden h-full items-center md:flex">
|
<div className="hidden h-full items-center md:flex">
|
||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
@@ -531,13 +554,4 @@ const DisplayAttendees = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BookingListItem;
|
export default BookingListItem;
|
||||||
@@ -13,9 +13,11 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
|
|||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
@@ -26,19 +28,15 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
|||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
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/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon, DatePicker } from "@calcom/ui";
|
import { Icon, HeadSeo } from "@calcom/ui";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
// import { timeZone } from "@lib/clock";
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|
||||||
|
|
||||||
import Gates, { Gate, GateState } from "@components/Gates";
|
import Gates, { Gate, GateState } from "@components/Gates";
|
||||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||||
import BookingDescription from "@components/booking/BookingDescription";
|
import BookingDescription from "@components/booking/BookingDescription";
|
||||||
import TimeOptions from "@components/booking/TimeOptions";
|
import TimeOptions from "@components/booking/TimeOptions";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
|
||||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||||
|
|
||||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||||
@@ -153,7 +151,10 @@ const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime: browsingDate?.startOf("month"),
|
startTime:
|
||||||
|
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||||
|
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||||
|
: browsingDate?.startOf("month"),
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
duration,
|
duration,
|
||||||
@@ -219,7 +220,7 @@ function TimezoneDropdown({
|
|||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
||||||
<p className="flex items-center text-sm font-medium">
|
<p className="flex items-center text-sm font-medium">
|
||||||
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
|
<Icon.FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{timeZone}
|
{timeZone}
|
||||||
{isTimeOptionsOpen ? (
|
{isTimeOptionsOpen ? (
|
||||||
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||||
@@ -256,6 +257,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||||
|
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
@@ -280,9 +282,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// TODO: Improve this;
|
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
|
||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
@@ -295,7 +294,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]);
|
||||||
|
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) : [];
|
||||||
|
|
||||||
@@ -308,6 +307,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
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.
|
||||||
|
|
||||||
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
const gates = [
|
const gates = [
|
||||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||||
@@ -341,7 +342,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex-col md:mx-4 lg:flex",
|
"flex flex-col md:mx-4",
|
||||||
shouldAlignCentrally ? "items-center" : "items-start",
|
shouldAlignCentrally ? "items-center" : "items-start",
|
||||||
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
|
||||||
)}>
|
)}>
|
||||||
@@ -356,54 +357,55 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="overflow-hidden md:flex">
|
<div className="overflow-hidden md:flex">
|
||||||
<div
|
{showEventTypeDetails && (
|
||||||
className={classNames(
|
<div
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
className={classNames(
|
||||||
"min-w-full md:w-[230px] md:min-w-[230px]",
|
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||||
)}>
|
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
)}>
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
<div className="flex items-start text-sm font-medium">
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
|
<div className="flex items-start text-sm font-medium">
|
||||||
<div>
|
<Icon.FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<div>
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
</p>
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
<input
|
</p>
|
||||||
type="number"
|
<input
|
||||||
min="1"
|
type="number"
|
||||||
max={eventType.recurringEvent.count}
|
min="1"
|
||||||
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
max={eventType.recurringEvent.count}
|
||||||
defaultValue={eventType.recurringEvent.count}
|
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
|
||||||
onChange={(event) => {
|
defaultValue={eventType.recurringEvent.count}
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
onChange={(event) => {
|
||||||
}}
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
/>
|
}}
|
||||||
<p className="inline">
|
/>
|
||||||
{t("occurrence", {
|
<p className="inline">
|
||||||
count: recurringEventCount,
|
{t("occurrence", {
|
||||||
})}
|
count: recurringEventCount,
|
||||||
</p>
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{stripeAppData.price > 0 && (
|
||||||
{stripeAppData.price > 0 && (
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<IntlProvider locale="en">
|
||||||
<IntlProvider locale="en">
|
<FormattedNumber
|
||||||
<FormattedNumber
|
value={stripeAppData.price / 100.0}
|
||||||
value={stripeAppData.price / 100.0}
|
style="currency"
|
||||||
style="currency"
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
/>
|
||||||
/>
|
</IntlProvider>
|
||||||
</IntlProvider>
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
{timezoneDropdown}
|
||||||
{timezoneDropdown}
|
</BookingDescription>
|
||||||
</BookingDescription>
|
|
||||||
|
|
||||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||||
<div>
|
<div>
|
||||||
<p
|
<p
|
||||||
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
|
||||||
@@ -411,12 +413,13 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
<p className="text-gray-500 line-through dark:text-darkgray-600">
|
||||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)*/}
|
)*/}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<SlotPicker
|
<SlotPicker
|
||||||
weekStart={
|
weekStart={
|
||||||
typeof profile.weekStart === "string"
|
typeof profile.weekStart === "string"
|
||||||
@@ -442,7 +445,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
|
{/* 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 />}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { LocationObject, LocationType } from "@calcom/core/location";
|
|||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import {
|
import {
|
||||||
useEmbedNonStylesConfig,
|
useEmbedNonStylesConfig,
|
||||||
|
useEmbedUiConfig,
|
||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
@@ -89,6 +90,7 @@ const BookingPage = ({
|
|||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { duration: queryDuration } = useRouterQuery("duration");
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -141,6 +143,7 @@ const BookingPage = ({
|
|||||||
isSuccessBookingPage: true,
|
isSuccessBookingPage: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -156,6 +159,7 @@ const BookingPage = ({
|
|||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("email"),
|
email: bookingForm.getValues("email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
|
formerTime: booking?.startTime.toString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -297,6 +301,39 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const bookEvent = (booking: BookingFormValues) => {
|
const bookEvent = (booking: BookingFormValues) => {
|
||||||
|
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||||
|
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||||
|
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Checking if custom inputs of type Phone number are valid to display error message on UI
|
||||||
|
if (eventType.customInputs.length) {
|
||||||
|
let isErrorFound = false;
|
||||||
|
eventType.customInputs.forEach((customInput) => {
|
||||||
|
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
|
||||||
|
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
|
||||||
|
try {
|
||||||
|
z.string({
|
||||||
|
errorMap: () => ({
|
||||||
|
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((val) => isValidPhoneNumber(val), {
|
||||||
|
message: "Phone number is invalid",
|
||||||
|
})
|
||||||
|
.parse(input?.value);
|
||||||
|
} catch (err) {
|
||||||
|
isErrorFound = true;
|
||||||
|
bookingForm.setError(`customInputs.${customInput.id}`, {
|
||||||
|
type: "custom",
|
||||||
|
message: "Invalid Phone number",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isErrorFound) return;
|
||||||
|
}
|
||||||
|
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||||
{ isTeamBooking: document.URL.includes("team/") }
|
{ isTeamBooking: document.URL.includes("team/") }
|
||||||
@@ -315,6 +352,22 @@ const BookingPage = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (eventType.customInputs.length > 0) {
|
||||||
|
// find all required custom inputs and ensure they are filled out in the booking form
|
||||||
|
const requiredCustomInputs = eventType.customInputs.filter((input) => input.required);
|
||||||
|
const missingRequiredCustomInputs = requiredCustomInputs.filter(
|
||||||
|
(input) => !booking?.customInputs?.[input.id]
|
||||||
|
);
|
||||||
|
if (missingRequiredCustomInputs.length > 0) {
|
||||||
|
missingRequiredCustomInputs.forEach((input) => {
|
||||||
|
bookingForm.setError(`customInputs.${input.id}`, {
|
||||||
|
type: "required",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (recurringDates.length) {
|
if (recurringDates.length) {
|
||||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||||
const recurringEventId = uuidv4();
|
const recurringEventId = uuidv4();
|
||||||
@@ -337,10 +390,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -368,10 +418,7 @@ const BookingPage = ({
|
|||||||
attendeeAddress: booking.attendeeAddress,
|
attendeeAddress: booking.attendeeAddress,
|
||||||
}),
|
}),
|
||||||
metadata,
|
metadata,
|
||||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
customInputs: bookingCustomInputs,
|
||||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
|
||||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
|
||||||
})),
|
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
@@ -406,6 +453,7 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
@@ -439,7 +487,7 @@ const BookingPage = ({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? "" : "sm:my-24",
|
isEmbed ? "" : "sm:my-24",
|
||||||
"my-0 max-w-3xl "
|
"my-0 max-w-3xl"
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -448,95 +496,97 @@ const BookingPage = ({
|
|||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"dark:border-darkgray-300 rounded-md sm:border"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
{showEventTypeDetails && (
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
{stripeAppData.price > 0 && (
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
{stripeAppData.price > 0 && (
|
||||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<IntlProvider locale="en">
|
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<FormattedNumber
|
<IntlProvider locale="en">
|
||||||
value={stripeAppData.price / 100.0}
|
<FormattedNumber
|
||||||
style="currency"
|
value={stripeAppData.price / 100.0}
|
||||||
currency={stripeAppData.currency.toUpperCase()}
|
style="currency"
|
||||||
/>
|
currency={stripeAppData.currency.toUpperCase()}
|
||||||
</IntlProvider>
|
/>
|
||||||
</p>
|
</IntlProvider>
|
||||||
)}
|
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
|
||||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
|
||||||
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
|
||||||
<p className="-ml-2 inline-block items-center px-2">
|
|
||||||
{getEveryFreqFor({
|
|
||||||
t,
|
|
||||||
recurringEvent: eventType.recurringEvent,
|
|
||||||
recurringCount: recurringEventCount,
|
|
||||||
})}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
)}
|
||||||
)}
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<div className="text-sm font-medium">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
{getEveryFreqFor({
|
||||||
{!rescheduleUid &&
|
t,
|
||||||
eventType.recurringEvent?.freq &&
|
recurringEvent: eventType.recurringEvent,
|
||||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
recurringCount: recurringEventCount,
|
||||||
return <p key={key}>{timeFormatted}</p>;
|
})}
|
||||||
})}
|
</p>
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
</div>
|
||||||
<div className="flex">
|
)}
|
||||||
<Tooltip
|
|
||||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
|
||||||
<p key={key}>{timeFormatted}</p>
|
|
||||||
))}>
|
|
||||||
<p className="dark:text-darkgray-600 text-sm">
|
|
||||||
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
|
||||||
</p>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{booking?.startTime && rescheduleUid && (
|
|
||||||
<div>
|
|
||||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
|
||||||
{t("former_time")}
|
|
||||||
</p>
|
|
||||||
<p className="line-through ">
|
|
||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
|
||||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<Icon.FiUser
|
<Icon.FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
<div className="text-sm font-medium">
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||||
? "text-rose-600"
|
{!rescheduleUid &&
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
eventType.recurringEvent?.freq &&
|
||||||
? "text-yellow-500"
|
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||||
: "text-bookinghighlight"
|
return <p key={key}>{timeFormatted}</p>;
|
||||||
}`}
|
})}
|
||||||
/>
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
<p
|
<div className="flex">
|
||||||
className={`${
|
<Tooltip
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||||
? "text-rose-600"
|
<p key={key}>{timeFormatted}</p>
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
))}>
|
||||||
? "text-yellow-500"
|
<p className="dark:text-darkgray-600 text-sm">
|
||||||
: "text-bookinghighlight"
|
+ {t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
} mb-2 font-medium`}>
|
</p>
|
||||||
{booking
|
</Tooltip>
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
</div>
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
)}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{booking?.startTime && rescheduleUid && (
|
||||||
</BookingDescription>
|
<div>
|
||||||
</div>
|
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||||
<div className="p-6 sm:w-1/2">
|
{t("former_time")}
|
||||||
|
</p>
|
||||||
|
<p className="line-through ">
|
||||||
|
<Icon.FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
|
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
|
<Icon.FiUser
|
||||||
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
className={`${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
} mb-2 font-medium`}>
|
||||||
|
{booking
|
||||||
|
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||||
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BookingDescription>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
|
||||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
@@ -573,7 +623,7 @@ const BookingPage = ({
|
|||||||
/>
|
/>
|
||||||
{bookingForm.formState.errors.email && (
|
{bookingForm.formState.errors.email && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||||
<p>{t("email_validation_error")}</p>
|
<p>{t("email_validation_error")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -614,8 +664,8 @@ const BookingPage = ({
|
|||||||
value={location.type}
|
value={location.type}
|
||||||
defaultChecked={i === 0}
|
defaultChecked={i === 0}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
|
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
|
||||||
{locationKeyToString(location)}
|
{t(locationKeyToString(location) ?? "")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
@@ -639,7 +689,7 @@ const BookingPage = ({
|
|||||||
{selectedLocationType === LocationType.Phone
|
{selectedLocationType === LocationType.Phone
|
||||||
? t("phone_number")
|
? t("phone_number")
|
||||||
: selectedLocationType === LocationType.AttendeeInPerson
|
: selectedLocationType === LocationType.AttendeeInPerson
|
||||||
? t("Address")
|
? t("address")
|
||||||
: ""}
|
: ""}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
@@ -666,7 +716,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.phone && (
|
{bookingForm.formState.errors.phone && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||||
<p>{t("invalid_number")}</p>
|
<p>{t("invalid_number")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -679,8 +729,11 @@ const BookingPage = ({
|
|||||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||||
<label
|
<label
|
||||||
htmlFor={"custom_" + input.id}
|
htmlFor={"custom_" + input.id}
|
||||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
className={classNames(
|
||||||
{input.label}
|
"mb-1 block text-sm font-medium text-gray-700 transition-colors dark:text-white",
|
||||||
|
bookingForm.formState.errors.customInputs?.[input.id] && "!text-red-700"
|
||||||
|
)}>
|
||||||
|
{input.label} {input.required && <span className="text-red-700">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||||
@@ -745,24 +798,46 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
{input.options && input.type === EventTypeCustomInputType.RADIO && (
|
||||||
<div className="">
|
<div className="flex">
|
||||||
<div className="flex">
|
<Group
|
||||||
<Group
|
name={`customInputs.${input.id}`}
|
||||||
onValueChange={(e) => {
|
required={input.required}
|
||||||
bookingForm.setValue(`customInputs.${input.id}`, e);
|
onValueChange={(e) => {
|
||||||
}}>
|
bookingForm.setValue(`customInputs.${input.id}`, e);
|
||||||
<>
|
}}>
|
||||||
{input.options.map((option, i) => (
|
<>
|
||||||
<RadioField
|
{input.options.map((option, i) => (
|
||||||
label={option.label}
|
<RadioField
|
||||||
key={`option.${i}.radio`}
|
label={option.label}
|
||||||
value={option.label}
|
key={`option.${input.id}.${i}.radio`}
|
||||||
id={`option.${i}.radio`}
|
value={option.label}
|
||||||
/>
|
id={`option.${input.id}.${i}.radio`}
|
||||||
))}
|
/>
|
||||||
</>
|
))}
|
||||||
</Group>
|
</>
|
||||||
</div>
|
{bookingForm.formState.errors.customInputs?.[input.id] && (
|
||||||
|
<div className="mt-px flex items-center text-xs text-red-700 ">
|
||||||
|
<p>{t("required")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{input.type === EventTypeCustomInputType.PHONE && (
|
||||||
|
<div>
|
||||||
|
<PhoneInput<BookingFormValues>
|
||||||
|
name={`customInputs.${input.id}`}
|
||||||
|
control={bookingForm.control}
|
||||||
|
placeholder={t("enter_phone_number")}
|
||||||
|
id={`customInputs.${input.id}`}
|
||||||
|
required={input.required}
|
||||||
|
/>
|
||||||
|
{bookingForm.formState.errors?.customInputs?.[input.id] && (
|
||||||
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
|
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||||
|
<p>{t("invalid_number")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -839,7 +914,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.smsReminderNumber && (
|
{bookingForm.formState.errors.smsReminderNumber && (
|
||||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||||
<p>{t("invalid_number")}</p>
|
<p>{t("invalid_number")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
/>
|
/>
|
||||||
{selectedLocation && LocationOptions}
|
{selectedLocation && LocationOptions}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowLocationModal(false);
|
setShowLocationModal(false);
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
<span className="w-16 sm:w-28 sm:text-left">
|
<span className="w-16 sm:w-28 sm:text-left">
|
||||||
{format(dayRange.startTime, timeFormat === 12)}
|
{format(dayRange.startTime, timeFormat === 12)}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-4">-</span>
|
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -164,7 +164,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
|||||||
<hr />
|
<hr />
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||||
<Icon.FiGlobe className="mr-2" />
|
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
value: EventTypeCustomInputType.RADIO,
|
value: EventTypeCustomInputType.RADIO,
|
||||||
label: t("radio"),
|
label: t("radio"),
|
||||||
},
|
},
|
||||||
|
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { selectedCustomInput } = props;
|
const { selectedCustomInput } = props;
|
||||||
@@ -133,8 +134,8 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
|||||||
value={selectedCustomInput?.id || -1}
|
value={selectedCustomInput?.id || -1}
|
||||||
{...register("id", { valueAsNumber: true })}
|
{...register("id", { valueAsNumber: true })}
|
||||||
/>
|
/>
|
||||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-4">
|
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
|
||||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
||||||
|
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
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 {
|
import {
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CustomInputItem,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -36,7 +37,19 @@ const generateHashedLink = (id: number) => {
|
|||||||
return uid;
|
return uid;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
|
const getRandomId = (length = 8) => {
|
||||||
|
return (
|
||||||
|
-1 *
|
||||||
|
parseInt(
|
||||||
|
Math.ceil(Math.random() * Date.now())
|
||||||
|
.toPrecision(length)
|
||||||
|
.toString()
|
||||||
|
.replace(".", "")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -51,7 +64,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
const removeCustom = (index: number) => {
|
||||||
@@ -64,6 +76,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
}, [eventType.users, hashedUrl, team?.id]);
|
}, [eventType.users, hashedUrl, team?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (eventType.customInputs) {
|
||||||
|
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
|
||||||
|
}
|
||||||
|
}, [eventType.customInputs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-8">
|
<div className="flex flex-col space-y-8">
|
||||||
{/**
|
{/**
|
||||||
@@ -75,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Label>{t("add_to_calendar")}</Label>
|
<Label>{t("add_to_calendar")}</Label>
|
||||||
<Link href="/apps/categories/calendar">
|
<Link
|
||||||
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900">
|
href="/apps/categories/calendar"
|
||||||
{t("add_another_calendar")}
|
target="_blank"
|
||||||
</a>
|
className="text-sm text-gray-600 hover:text-gray-900">
|
||||||
|
{t("add_another_calendar")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-1 w-full">
|
<div className="-mt-1 w-full">
|
||||||
@@ -239,13 +258,28 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2 flex">
|
||||||
|
<Checkbox
|
||||||
|
description={t("disable_success_page")}
|
||||||
|
// Disable if it's not Self Hosted or if the redirect url is not set
|
||||||
|
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
||||||
|
{...formMethods.register("metadata.disableSuccessPage")}
|
||||||
|
/>
|
||||||
|
{/*TODO: Extract it out into a component when used more than once*/}
|
||||||
|
{!IS_SELF_HOSTED && (
|
||||||
|
<Link href="https://cal.com/pricing" target="_blank">
|
||||||
|
<Badge variant="orange" className="ml-2">
|
||||||
|
Platform Only
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
@@ -374,7 +408,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<Controller
|
<Controller
|
||||||
name="customInputs"
|
name="customInputs"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={eventType.customInputs.sort((a, b) => a.id - b.id) || []}
|
defaultValue={customInputs}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
@@ -386,13 +420,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
selectedCustomInput={selectedCustomInput}
|
selectedCustomInput={selectedCustomInput}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
const customInput: CustomInputParsed = {
|
const customInput: CustomInputParsed = {
|
||||||
id: -1,
|
id: getRandomId(),
|
||||||
eventTypeId: -1,
|
eventTypeId: -1,
|
||||||
label: values.label,
|
label: values.label,
|
||||||
placeholder: values.placeholder,
|
placeholder: values.placeholder,
|
||||||
required: values.required,
|
required: values.required,
|
||||||
type: values.type,
|
type: values.type,
|
||||||
options: values.options,
|
options: values.options,
|
||||||
|
hasToBeCreated: true,
|
||||||
};
|
};
|
||||||
if (selectedCustomInput) {
|
if (selectedCustomInput) {
|
||||||
selectedCustomInput.label = customInput.label;
|
selectedCustomInput.label = customInput.label;
|
||||||
@@ -400,6 +435,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
selectedCustomInput.required = customInput.required;
|
selectedCustomInput.required = customInput.required;
|
||||||
selectedCustomInput.type = customInput.type;
|
selectedCustomInput.type = customInput.type;
|
||||||
selectedCustomInput.options = customInput.options || undefined;
|
selectedCustomInput.options = customInput.options || undefined;
|
||||||
|
selectedCustomInput.hasToBeCreated = false;
|
||||||
// Update by id
|
// Update by id
|
||||||
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
|
||||||
customInputs[inputIndex] = selectedCustomInput;
|
customInputs[inputIndex] = selectedCustomInput;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
|
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
|
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
|
|
||||||
function AppCardWrapper({
|
function AppCardWrapper({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import { PeriodType } from "@calcom/prisma/client";
|
|||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
|
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
|
||||||
@@ -197,7 +197,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
|||||||
label={t("minimum_booking_notice")}
|
label={t("minimum_booking_notice")}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="120"
|
placeholder="120"
|
||||||
className="mr-2 mb-0 h-[38px] rounded-[4px]"
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
{...formMethods.register("minimumBookingNoticeInDurationType", {
|
{...formMethods.register("minimumBookingNoticeInDurationType", {
|
||||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onMinimumNoticeChange(event.target.value),
|
onMinimumNoticeChange(event.target.value),
|
||||||
@@ -327,7 +327,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.type === "RANGE" && (
|
{period.type === "RANGE" && (
|
||||||
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
||||||
<Controller
|
<Controller
|
||||||
name="periodDates"
|
name="periodDates"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -347,7 +347,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span> : null}
|
{period.suffix ? (
|
||||||
|
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2"> {period.suffix}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -410,7 +412,9 @@ const BookingLimits = () => {
|
|||||||
.map(([key, bookingAmount]) => {
|
.map(([key, bookingAmount]) => {
|
||||||
const bookingLimitKey = key as BookingLimitsKey;
|
const bookingLimitKey = key as BookingLimitsKey;
|
||||||
return (
|
return (
|
||||||
<div className="mb-2 flex items-center space-x-2 text-sm" key={bookingLimitKey}>
|
<div
|
||||||
|
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
|
||||||
|
key={bookingLimitKey}>
|
||||||
<Input
|
<Input
|
||||||
id={`${bookingLimitKey}-limit`}
|
id={`${bookingLimitKey}-limit`}
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { EventTypeSetupInfered } from "pages/event-types/[type]";
|
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
|
|
||||||
import RecurringEventController from "./RecurringEventController";
|
import RecurringEventController from "./RecurringEventController";
|
||||||
|
|
||||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
|
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const stripeAppData = getStripeAppData(eventType);
|
||||||
|
|
||||||
const requirePayment = stripeAppData.price > 0;
|
const requirePayment = stripeAppData.price > 0;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { Trans } from "next-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { MultiValue } from "react-select";
|
import { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||||
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 { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
import { slugify } from "@lib/slugify";
|
import { slugify } from "@lib/slugify";
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ type OptionTypeBase = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
props: Pick<EventTypeSetupInfered, "eventType" | "locationOptions" | "team" | "teamMembers">
|
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
@@ -169,7 +171,7 @@ export const EventSetupTab = (
|
|||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
|
||||||
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -195,6 +197,23 @@ export const EventSetupTab = (
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{validLocations.some((location) => location.type === MeetLocationType) && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
|
<p>
|
||||||
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
|
Change it{" "}
|
||||||
|
<Link
|
||||||
|
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
|
||||||
|
className="underline">
|
||||||
|
here.
|
||||||
|
</Link>{" "}
|
||||||
|
We will fall back to Cal video if you do not change it.
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||||
<li>
|
<li>
|
||||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||||
@@ -250,23 +269,26 @@ export const EventSetupTab = (
|
|||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="h-auto !min-h-[36px] text-sm"
|
className="h-auto !min-h-[36px] text-sm"
|
||||||
options={multipleDurationOptions}
|
options={multipleDurationOptions}
|
||||||
|
value={selectedMultipleDuration}
|
||||||
onChange={(options) => {
|
onChange={(options) => {
|
||||||
const values = options
|
let newOptions = [...options];
|
||||||
.map((opt) => opt.value)
|
newOptions = newOptions.sort((a, b) => {
|
||||||
.sort(function (a, b) {
|
return a?.value - b?.value;
|
||||||
return a - b;
|
});
|
||||||
});
|
const values = newOptions.map((opt) => opt.value);
|
||||||
setMultipleDuration(values);
|
setMultipleDuration(values);
|
||||||
setSelectedMultipleDuration(options);
|
setSelectedMultipleDuration(newOptions);
|
||||||
if (!options.find((opt) => opt.value === defaultDuration?.value)) {
|
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||||
if (options.length > 0) {
|
if (newOptions.length > 0) {
|
||||||
setDefaultDuration(options[0]);
|
setDefaultDuration(newOptions[0]);
|
||||||
|
formMethods.setValue("length", newOptions[0].value);
|
||||||
} else {
|
} else {
|
||||||
setDefaultDuration(null);
|
setDefaultDuration(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (options.length === 1 && defaultDuration === null) {
|
if (newOptions.length === 1 && defaultDuration === null) {
|
||||||
setDefaultDuration(options[0]);
|
setDefaultDuration(newOptions[0]);
|
||||||
|
formMethods.setValue("length", newOptions[0].value);
|
||||||
}
|
}
|
||||||
formMethods.setValue("metadata.multipleDuration", values);
|
formMethods.setValue("metadata.multipleDuration", values);
|
||||||
}}
|
}}
|
||||||
@@ -287,7 +309,7 @@ export const EventSetupTab = (
|
|||||||
setDefaultDuration(
|
setDefaultDuration(
|
||||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||||
);
|
);
|
||||||
formMethods.setValue("length", Number(option?.value));
|
if (option) formMethods.setValue("length", option.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,14 +317,11 @@ export const EventSetupTab = (
|
|||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
name="length"
|
|
||||||
type="number"
|
type="number"
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
onChange={(e) => {
|
{...formMethods.register("length")}
|
||||||
formMethods.setValue("length", Number(e.target.value));
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
@@ -326,6 +345,7 @@ export const EventSetupTab = (
|
|||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="locations"
|
name="locations"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -342,7 +362,7 @@ export const EventSetupTab = (
|
|||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
defaultValues={formMethods.getValues("locations")}
|
defaultValues={formMethods.getValues("locations")}
|
||||||
selection={
|
selection={
|
||||||
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
|
selectedLocation ? { value: selectedLocation.value, label: t(selectedLocation.label) } : undefined
|
||||||
}
|
}
|
||||||
setSelectedLocation={setSelectedLocation}
|
setSelectedLocation={setSelectedLocation}
|
||||||
setEditingLocationType={setEditingLocationType}
|
setEditingLocationType={setEditingLocationType}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { SchedulingType } from "@prisma/client/";
|
import { SchedulingType } from "@prisma/client/";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
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 { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IMemberToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
@@ -25,8 +26,7 @@ export const EventTeamTab = ({
|
|||||||
eventType,
|
eventType,
|
||||||
team,
|
team,
|
||||||
teamMembers,
|
teamMembers,
|
||||||
currentUserMembership,
|
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => {
|
||||||
}: Pick<EventTypeSetupInfered, "eventType" | "teamMembers" | "team" | "currentUserMembership">) => {
|
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -101,48 +101,3 @@ export const EventTeamTab = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TeamProfileCard = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
hasPermsToDelete,
|
|
||||||
email,
|
|
||||||
username,
|
|
||||||
}: {
|
|
||||||
id: number;
|
|
||||||
name: string | null;
|
|
||||||
email: string;
|
|
||||||
username: string | null;
|
|
||||||
hasPermsToDelete: boolean;
|
|
||||||
}) => {
|
|
||||||
const formMethods = useFormContext<FormValues>();
|
|
||||||
const avatar = `${WEBAPP_URL}/${username}/avatar.png`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center border border-gray-300 px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md">
|
|
||||||
<Avatar imageSrc={avatar} gravatarFallbackMd5="ajsdkwakhsd1231" size="md" alt="Team Image" />
|
|
||||||
<div className="flex flex-col pl-3">
|
|
||||||
<div className="">
|
|
||||||
<span className="pr-2 text-sm font-semibold leading-none text-black">{name}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-normal leading-normal text-gray-700">{email}</p>
|
|
||||||
</div>
|
|
||||||
{hasPermsToDelete && (
|
|
||||||
<div className="ml-auto">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="icon"
|
|
||||||
color="destructive"
|
|
||||||
StartIcon={Icon.FiTrash}
|
|
||||||
onClick={() => {
|
|
||||||
formMethods.setValue(
|
|
||||||
"users",
|
|
||||||
formMethods.getValues("users").filter((user) => user !== id.toString())
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
|
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Webhook } from "@calcom/prisma/client";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
|
export const EventTeamWebhooksTab = ({
|
||||||
|
eventType,
|
||||||
|
team,
|
||||||
|
}: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||||
|
|
||||||
|
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||||
|
variant: "other",
|
||||||
|
onlyInstalled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
|
||||||
|
|
||||||
|
const subscriberUrlReserved = (subscriberUrl: string, id?: string): boolean => {
|
||||||
|
return !!webhooks?.find(
|
||||||
|
(webhook) => webhook.subscriberUrl === subscriberUrl && (!id || webhook.id !== id)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
setEditModalOpen(false);
|
||||||
|
await utils.viewer.webhook.list.invalidate();
|
||||||
|
showToast(t("webhook_updated_successfully"), "success");
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast(`${error.message}`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||||
|
async onSuccess() {
|
||||||
|
showToast(t("webhook_created_successfully"), "success");
|
||||||
|
await utils.viewer.webhook.list.invalidate();
|
||||||
|
setCreateModalOpen(false);
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
showToast(`${error.message}`, "error");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
|
||||||
|
if (subscriberUrlReserved(values.subscriberUrl, values.id)) {
|
||||||
|
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.payloadTemplate) {
|
||||||
|
values.payloadTemplate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createWebhookMutation.mutate({
|
||||||
|
subscriberUrl: values.subscriberUrl,
|
||||||
|
eventTriggers: values.eventTriggers,
|
||||||
|
active: values.active,
|
||||||
|
payloadTemplate: values.payloadTemplate,
|
||||||
|
secret: values.secret,
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const NewWebhookButton = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
data-testid="new_webhook"
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
|
{t("new_webhook")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{team && webhooks && !isLoading && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
{webhooks.length ? (
|
||||||
|
<>
|
||||||
|
<div className="mt-4 mb-8 rounded-md border">
|
||||||
|
{webhooks.map((webhook, index) => {
|
||||||
|
return (
|
||||||
|
<WebhookListItem
|
||||||
|
key={webhook.id}
|
||||||
|
webhook={webhook}
|
||||||
|
lastItem={webhooks.length === index + 1}
|
||||||
|
onEditWebhook={() => {
|
||||||
|
setEditModalOpen(true);
|
||||||
|
setWebhookToEdit(webhook);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<NewWebhookButton />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={TbWebhook}
|
||||||
|
headline={t("create_your_first_webhook")}
|
||||||
|
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||||
|
buttonRaw={<NewWebhookButton />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New webhook dialog */}
|
||||||
|
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||||
|
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
|
||||||
|
<WebhookForm
|
||||||
|
onSubmit={onCreateWebhook}
|
||||||
|
onCancel={() => setCreateModalOpen(false)}
|
||||||
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/* Edit webhook dialog */}
|
||||||
|
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||||
|
<DialogContent title={t("edit_webhook")}>
|
||||||
|
<WebhookForm
|
||||||
|
webhook={webhookToEdit}
|
||||||
|
apps={installedApps?.items.map((app) => app.slug)}
|
||||||
|
onCancel={() => setEditModalOpen(false)}
|
||||||
|
onSubmit={(values: WebhookFormSubmitData) => {
|
||||||
|
if (subscriberUrlReserved(values.subscriberUrl, webhookToEdit?.id || "")) {
|
||||||
|
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.changeSecret) {
|
||||||
|
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.payloadTemplate) {
|
||||||
|
values.payloadTemplate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
editWebhookMutation.mutate({
|
||||||
|
id: webhookToEdit?.id || "",
|
||||||
|
subscriberUrl: values.subscriberUrl,
|
||||||
|
eventTriggers: values.eventTriggers,
|
||||||
|
active: values.active,
|
||||||
|
payloadTemplate: values.payloadTemplate,
|
||||||
|
secret: values.secret,
|
||||||
|
eventTypeId: webhookToEdit?.eventTypeId || undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { TFunction } from "next-i18next";
|
import { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import { Loader } from "react-feather";
|
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
|
import { TbWebhook } from "react-icons/tb";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -32,14 +32,13 @@ import {
|
|||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { ClientSuspense } from "@components/ClientSuspense";
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
eventType: EventTypeSetupInfered["eventType"];
|
eventType: EventTypeSetupProps["eventType"];
|
||||||
currentUserMembership: EventTypeSetupInfered["currentUserMembership"];
|
currentUserMembership: EventTypeSetupProps["currentUserMembership"];
|
||||||
team: EventTypeSetupInfered["team"];
|
team: EventTypeSetupProps["team"];
|
||||||
disableBorder?: boolean;
|
disableBorder?: boolean;
|
||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
@@ -150,13 +149,20 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team)
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "scheduling_type",
|
name: "scheduling_type",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: Icon.FiUsers,
|
icon: Icon.FiUsers,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||||
});
|
});
|
||||||
|
navigation.push({
|
||||||
|
name: "webhooks",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
|
icon: TbWebhook,
|
||||||
|
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
||||||
|
|
||||||
@@ -294,7 +300,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<ClientSuspense fallback={<Loader />}>
|
<Suspense fallback={<Icon.FiLoader />}>
|
||||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
@@ -317,7 +323,7 @@ function EventTypeSingleLayout({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ClientSuspense>
|
</Suspense>
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function RecurringEventController({
|
|||||||
{recurringEventState && (
|
{recurringEventState && (
|
||||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -89,7 +89,7 @@ export default function RecurringEventController({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
@@ -105,7 +105,7 @@ export default function RecurringEventController({
|
|||||||
setRecurringEventState(newVal);
|
setRecurringEventState(newVal);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="mr-2 text-neutral-900">
|
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">
|
||||||
{t("events", {
|
{t("events", {
|
||||||
count: recurringEventState.count,
|
count: recurringEventState.count,
|
||||||
})}
|
})}
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import { StepCard, Steps } from "../../..";
|
import { StepCard, Steps } from "@calcom/ui";
|
||||||
|
|
||||||
export default function WizardLayout({
|
export default function WizardLayout({
|
||||||
children,
|
children,
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
We don't have many layouts currently however, if they are relevant to a features - please put them in their relvant component folder
|
||||||
|
in `web/components` if you believe they will be reused in other apps please place them in `packages/features/[feature]`
|
||||||
|
|
||||||
|
Thanks Sean 🔥
|
||||||
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
|
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 rtl:mr-2"
|
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
@@ -186,13 +186,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 rtl:mr-2"
|
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
|
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { ShieldCheckIcon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<ShieldCheckIcon className="h-6 w-6 text-white" />
|
<Icon.ShieldCheckIcon className="h-6 w-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export function CalendarSwitch(props: {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex space-x-2 py-1">
|
<div className="flex space-x-2 py-1 rtl:space-x-reverse">
|
||||||
<Switch
|
<Switch
|
||||||
key={props.externalId}
|
key={props.externalId}
|
||||||
name="enabled"
|
name="enabled"
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
|
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
|
||||||
{t("disable")}
|
{t("disable")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="secondary" onClick={onCancel}>
|
<Button color="secondary" onClick={onCancel}>
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 rtl:mr-2"
|
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||||
onClick={handleSetup}
|
onClick={handleSetup}
|
||||||
disabled={password.length === 0 || isSubmitting}>
|
disabled={password.length === 0 || isSubmitting}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
@@ -179,13 +179,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
|||||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="ltr:ml-2 rtl:mr-2"
|
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
|
||||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||||
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
|
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
|
||||||
{t("enable")}
|
{t("enable")}
|
||||||
</Button>
|
</Button>
|
||||||
</WithStep>
|
</WithStep>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||||
|
import { Credits, HeadSeo } from "@calcom/ui";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -23,6 +22,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
|
<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-center text-3xl text-neutral-900">{props.heading}</h2>}
|
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
|
||||||
</div>
|
</div>
|
||||||
@@ -35,7 +35,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
|||||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
|
<div className="mt-8 text-center text-sm text-neutral-600">
|
||||||
|
{props.footerText}
|
||||||
|
<Credits />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,14 +34,8 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={idx} className="-mr-2 inline-block">
|
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
|
||||||
{item.href ? (
|
{item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
|
||||||
<Link href={item.href}>
|
|
||||||
<a>{avatar}</a>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
avatar
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
interface ILinkTextProps {
|
|
||||||
href: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
classNameChildren?: string;
|
|
||||||
target?: string;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* This component had to be made in order to make i18n work with next/link
|
|
||||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
|
||||||
**/
|
|
||||||
export const LinkText = (props: ILinkTextProps) => {
|
|
||||||
const { target, href, children, classNameChildren, ...moreProps } = props;
|
|
||||||
return (
|
|
||||||
<Link href={href || ""} {...moreProps}>
|
|
||||||
<a target={target || ""} className={classNameChildren}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
|
|||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
return (
|
return (
|
||||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
||||||
<Link href={POWERED_BY_URL}>
|
<Link
|
||||||
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
href={POWERED_BY_URL}
|
||||||
{t("powered_by")}{" "}
|
target="_blank"
|
||||||
{
|
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
{t("powered_by")}{" "}
|
||||||
<img
|
<img
|
||||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||||
src="/cal-logo-word.svg"
|
src="/cal-logo-word.svg"
|
||||||
alt="Cal.com Logo"
|
alt="Cal.com Logo"
|
||||||
/>
|
/>
|
||||||
}
|
<img
|
||||||
{
|
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
src="/cal-logo-word-dark.svg"
|
||||||
<img
|
alt="Cal.com Logo"
|
||||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
/>
|
||||||
src="/cal-logo-word-dark.svg"
|
|
||||||
alt="Cal.com Logo"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,17 +11,7 @@ import { User } from "@calcom/prisma/client";
|
|||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import {
|
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
StarIconSolid,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
@@ -35,23 +25,7 @@ interface ICustomUsernameProps {
|
|||||||
setInputUsernameValue: (value: string) => void;
|
setInputUsernameValue: (value: string) => void;
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<
|
user: Pick<User, "username" | "metadata">;
|
||||||
User,
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "bio"
|
|
||||||
| "avatar"
|
|
||||||
| "timeZone"
|
|
||||||
| "weekStart"
|
|
||||||
| "hideBranding"
|
|
||||||
| "theme"
|
|
||||||
| "plan"
|
|
||||||
| "brandColor"
|
|
||||||
| "darkBrandColor"
|
|
||||||
| "timeFormat"
|
|
||||||
| "metadata"
|
|
||||||
>;
|
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +187,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<div className="flex justify-items-center">
|
<div className="flex justify-items-center">
|
||||||
<Label htmlFor="username">{t("username")}</Label>
|
<Label htmlFor="username">{t("username")}</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex rounded-md">
|
<div className="flex rounded-md">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
|
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
|
||||||
@@ -256,7 +230,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
isInputUsernamePremium ? "text-orange-400" : "",
|
isInputUsernamePremium ? "text-orange-400" : "",
|
||||||
usernameIsAvailable ? "" : ""
|
usernameIsAvailable ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
{isInputUsernamePremium ? <Icon.StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||||
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
|
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
Icon,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
TextField,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
currentUsername: string | undefined;
|
currentUsername: string | undefined;
|
||||||
@@ -77,10 +87,9 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="flex flex-row">
|
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="mx-2"
|
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
data-testid="update-username-btn">
|
data-testid="update-username-btn">
|
||||||
{t("update")}
|
{t("update")}
|
||||||
@@ -88,7 +97,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="mx-2"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (currentUsername) {
|
if (currentUsername) {
|
||||||
setInputUsernameValue(currentUsername);
|
setInputUsernameValue(currentUsername);
|
||||||
@@ -110,26 +118,20 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div className="flex rounded-md">
|
||||||
<Label htmlFor="username">{t("username")}</Label>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 flex rounded-md">
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
"inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"
|
|
||||||
)}>
|
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
|
|
||||||
</span>
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<Input
|
<TextField
|
||||||
ref={usernameRef}
|
ref={usernameRef}
|
||||||
name="username"
|
name="username"
|
||||||
value={inputUsernameValue}
|
value={inputUsernameValue}
|
||||||
|
addOnLeading={
|
||||||
|
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
||||||
|
}
|
||||||
autoComplete="none"
|
autoComplete="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-0 mt-0 h-6 rounded-md rounded-l-none",
|
"mb-0 mt-0 h-6 rounded-md ltr:rounded-l-none rtl:rounded-r-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"
|
||||||
: ""
|
: ""
|
||||||
@@ -141,18 +143,18 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
data-testid="username-input"
|
data-testid="username-input"
|
||||||
/>
|
/>
|
||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-0 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")}>
|
||||||
{usernameIsAvailable ? <Icon.FiCheck className="mt-[2px] w-6" /> : <></>}
|
{usernameIsAvailable ? <Icon.FiCheck className="w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden md:inline">
|
<div className="mt-5 hidden md:inline">
|
||||||
<ActionButtons />
|
<ActionButtons />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
|
{markAsError && <p className="mt-1 text-xs text-red-500">{t("username_already_taken")}</p>}
|
||||||
|
|
||||||
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
|
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
|
||||||
<div className="mt-2 flex justify-end md:hidden">
|
<div className="mt-2 flex justify-end md:hidden">
|
||||||
|
|||||||
@@ -14,23 +14,7 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
|
|||||||
interface UsernameAvailabilityFieldProps {
|
interface UsernameAvailabilityFieldProps {
|
||||||
onSuccessMutation?: () => void;
|
onSuccessMutation?: () => void;
|
||||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||||
user: Pick<
|
user: Pick<User, "username" | "metadata">;
|
||||||
User,
|
|
||||||
| "username"
|
|
||||||
| "name"
|
|
||||||
| "email"
|
|
||||||
| "bio"
|
|
||||||
| "avatar"
|
|
||||||
| "timeZone"
|
|
||||||
| "weekStart"
|
|
||||||
| "hideBranding"
|
|
||||||
| "theme"
|
|
||||||
| "plan"
|
|
||||||
| "brandColor"
|
|
||||||
| "darkBrandColor"
|
|
||||||
| "timeFormat"
|
|
||||||
| "metadata"
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
export const UsernameAvailabilityField = ({
|
export const UsernameAvailabilityField = ({
|
||||||
onSuccessMutation,
|
onSuccessMutation,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function Select<
|
|||||||
IndicatorSeparator: () => null,
|
IndicatorSeparator: () => null,
|
||||||
Input: InputComponent,
|
Input: InputComponent,
|
||||||
}}
|
}}
|
||||||
className={classNames("text-sm", className)}
|
className={classNames("border-0 text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import React, { ReactNode, useState } from "react";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
|
|
||||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
defaultChecked?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const RadioArea = (props: RadioAreaProps) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
className={classNames(
|
|
||||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
|
||||||
props.checked && "border-brand",
|
|
||||||
props.className
|
|
||||||
)}>
|
|
||||||
<input
|
|
||||||
onChange={(e) => {
|
|
||||||
if (typeof props.onChange === "function") {
|
|
||||||
props.onChange(e.target.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
checked={props.checked}
|
|
||||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
|
||||||
name={props.name}
|
|
||||||
value={props.value}
|
|
||||||
type="radio"
|
|
||||||
/>
|
|
||||||
{props.children}
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChildrenProps = {
|
|
||||||
props: RadioAreaProps;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
|
||||||
children: ChildrenProps | ChildrenProps[];
|
|
||||||
name?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
|
||||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
|
||||||
|
|
||||||
const changeHandler = (value: string, idx: number) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange(value);
|
|
||||||
}
|
|
||||||
setCheckedIdx(idx);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...passThroughProps}>
|
|
||||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
|
||||||
if (checkedIdx === null && child.props.defaultChecked) {
|
|
||||||
setCheckedIdx(idx);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Item
|
|
||||||
{...child.props}
|
|
||||||
key={idx}
|
|
||||||
name={name}
|
|
||||||
checked={idx === checkedIdx}
|
|
||||||
onChange={(value: string) => changeHandler(value, idx)}>
|
|
||||||
{child.props.children}
|
|
||||||
</Item>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item = RadioArea;
|
|
||||||
const Group = RadioAreaGroup;
|
|
||||||
|
|
||||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui";
|
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
|
|
||||||
|
|
||||||
interface OptionProps
|
|
||||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
|
||||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const {
|
|
||||||
options,
|
|
||||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
|
||||||
placeholder = t("select"),
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
|
||||||
options.find((option: OptionProps) => option.value === value)?.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible className={classNames("w-full", props.className)}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames(
|
|
||||||
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
|
|
||||||
disabled && "cursor-default bg-gray-200 focus:ring-0 "
|
|
||||||
)}>
|
|
||||||
{getLabel(props.value) ?? placeholder}
|
|
||||||
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
|
||||||
{options.map((option) => (
|
|
||||||
<RadioArea
|
|
||||||
{...option}
|
|
||||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
|
|
||||||
defaultChecked={props.value === option.value}>
|
|
||||||
<strong className="mb-1 block">{option.label}</strong>
|
|
||||||
<p>{option.description}</p>
|
|
||||||
</RadioArea>
|
|
||||||
))}
|
|
||||||
</RadioAreaGroup>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Select;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./RadioAreaGroup";
|
|
||||||
export * from "./Select";
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import parser from "accept-language-parser";
|
import parser from "accept-language-parser";
|
||||||
import { IncomingMessage } from "http";
|
import { IncomingMessage } from "http";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import { Maybe } from "@calcom/trpc/server";
|
import { Maybe } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
@@ -17,6 +16,7 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
|
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
|
||||||
|
const prisma = (await import("@calcom/prisma")).default;
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const preferredLocale = getLocaleFromHeaders(req);
|
const preferredLocale = getLocaleFromHeaders(req);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { UserPlan } from "@prisma/client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: It should be exposed at a single place.
|
|
||||||
*/
|
|
||||||
export function useExposePlanGlobally(plan: UserPlan) {
|
|
||||||
// Don't wait for component to mount. Do it ASAP. Delaying it would delay UI Configuration.
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
// This variable is used by embed-iframe to determine if we should allow UI configuration
|
|
||||||
window.CalComPlan = plan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -89,6 +89,12 @@ const nextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: !!process.env.CI,
|
ignoreDuringBuilds: !!process.env.CI,
|
||||||
},
|
},
|
||||||
|
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||||
|
// modularizeImports: {
|
||||||
|
// "@calcom/ui": {
|
||||||
|
// transform: "@calcom/ui/components/{{member}}",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
@@ -227,6 +233,11 @@ const nextConfig = {
|
|||||||
destination: "/404",
|
destination: "/404",
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/booking/direct/:action/:email/:bookingUid/:oldToken",
|
||||||
|
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
|
||||||
|
permanent: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.3.6",
|
"version": "2.4.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -61,12 +61,13 @@
|
|||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.0.19",
|
"@vercel/og": "^0.0.21",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dotenv-cli": "^6.0.0",
|
"dotenv-cli": "^6.0.0",
|
||||||
|
"eslint-config-next": "^13.1.1",
|
||||||
"googleapis": "^84.0.0",
|
"googleapis": "^84.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@@ -80,15 +81,14 @@
|
|||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"micro": "^9.4.1",
|
"micro": "^9.4.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.1",
|
||||||
"next-auth": "^4.10.3",
|
"next-auth": "^4.10.3",
|
||||||
"next-axiom": "^0.10.0",
|
"next-axiom": "^0.16.0",
|
||||||
"next-collect": "^0.2.1",
|
"next-collect": "^0.2.1",
|
||||||
"next-i18next": "^11.3.0",
|
"next-i18next": "^11.3.0",
|
||||||
"next-mdx-remote": "^4.1.0",
|
|
||||||
"next-seo": "^4.26.0",
|
"next-seo": "^4.26.0",
|
||||||
"next-themes": "^0.2.0",
|
"next-themes": "^0.2.0",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^10.0.0",
|
||||||
"nock": "^13.2.8",
|
"nock": "^13.2.8",
|
||||||
"nodemailer": "^6.7.8",
|
"nodemailer": "^6.7.8",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web3": "^1.7.5",
|
"web3": "^1.7.5",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.20.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.19.6",
|
||||||
|
|||||||
+47
-51
@@ -4,12 +4,10 @@ import { useRouter } from "next/router";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon, HeadSeo } from "@calcom/ui";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
import { HeadSeo } from "@components/seo/head-seo";
|
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
export default function Custom404() {
|
export default function Custom404() {
|
||||||
@@ -100,26 +98,26 @@ export default function Custom404() {
|
|||||||
|
|
||||||
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
||||||
<li className="px-4 py-2">
|
<li className="px-4 py-2">
|
||||||
<Link href={`${DEVELOPER_DOCS}/self-hosting/installation`}>
|
<Link
|
||||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
href={`${DEVELOPER_DOCS}/self-hosting/installation`}
|
||||||
<div className="flex-shrink-0">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
<div className="flex-shrink-0">
|
||||||
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||||
|
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">
|
||||||
|
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||||
|
<span className="absolute inset-0" aria-hidden="true" />
|
||||||
|
{t("prisma_studio_tip")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
<div className="min-w-0 flex-1">
|
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
||||||
<h3 className="text-base font-medium text-gray-900">
|
</div>
|
||||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
<div className="flex-shrink-0 self-center">
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
{t("prisma_studio_tip")}
|
</div>
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 self-center">
|
|
||||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li className="px-4 py-2">
|
<li className="px-4 py-2">
|
||||||
@@ -169,11 +167,11 @@ export default function Custom404() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Link href={`${WEBSITE_URL}/enterprise`}>
|
<Link
|
||||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
href={`${WEBSITE_URL}/enterprise`}
|
||||||
{t("contact_sales")}
|
className="text-base font-medium text-black hover:text-gray-500">
|
||||||
<span aria-hidden="true"> →</span>
|
{t("contact_sales")}
|
||||||
</a>
|
<span aria-hidden="true"> →</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,26 +243,26 @@ export default function Custom404() {
|
|||||||
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
|
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
|
||||||
{links.map((link, linkIdx) => (
|
{links.map((link, linkIdx) => (
|
||||||
<li key={linkIdx} className="px-4 py-2">
|
<li key={linkIdx} className="px-4 py-2">
|
||||||
<Link href={link.href}>
|
<Link
|
||||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
href={link.href}
|
||||||
<div className="flex-shrink-0">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
<div className="flex-shrink-0">
|
||||||
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||||
|
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h3 className="text-base font-medium text-gray-900">
|
||||||
|
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||||
|
<span className="absolute inset-0" aria-hidden="true" />
|
||||||
|
{link.title}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</h3>
|
||||||
<div className="min-w-0 flex-1">
|
<p className="text-base text-gray-500">{link.description}</p>
|
||||||
<h3 className="text-base font-medium text-gray-900">
|
</div>
|
||||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
<div className="flex-shrink-0 self-center">
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
{link.title}
|
</div>
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<p className="text-base text-gray-500">{link.description}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex-shrink-0 self-center">
|
|
||||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -315,11 +313,9 @@ export default function Custom404() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Link href="/">
|
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
{t("or_go_back_home")}
|
||||||
{t("or_go_back_home")}
|
<span aria-hidden="true"> →</span>
|
||||||
<span aria-hidden="true"> →</span>
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
-13
@@ -1,32 +1,48 @@
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button, Icon, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
export default function Error500() {
|
export default function Error500() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen bg-gray-100">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Something unexpected occurred | {APP_NAME}</title>
|
<title>Something unexpected occurred | {APP_NAME}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="m-auto text-center">
|
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
|
||||||
<h1 className="font-cal text-[250px] text-gray-900">
|
<h1 className="font-cal text-6xl text-black">500</h1>
|
||||||
5
|
<h2 className="mt-6 text-2xl font-medium text-black">It's not you, it's us.</h2>
|
||||||
{
|
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src="/error.svg" className="-mt-10 inline w-60" alt="0" />
|
|
||||||
}
|
|
||||||
0
|
|
||||||
</h1>
|
|
||||||
<h2 className="mb-2 -mt-16 text-3xl text-gray-600">It's not you, it's us.</h2>
|
|
||||||
<p className="mb-4 max-w-2xl text-gray-500">
|
|
||||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||||
away for you.
|
away for you.
|
||||||
</p>
|
</p>
|
||||||
|
{router.query.error && (
|
||||||
|
<div className="mb-8 flex flex-col">
|
||||||
|
<p className="mb-4 max-w-2xl text-sm text-gray-600">
|
||||||
|
Please provide the following text when contacting support to better help you:
|
||||||
|
</p>
|
||||||
|
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
|
||||||
|
{router.query.error}
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||||
|
StartIcon={Icon.FiCopy}
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(router.query.error as string);
|
||||||
|
showToast("Link copied!", "success");
|
||||||
|
}}>
|
||||||
|
{t("copy")}
|
||||||
|
</Button>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||||
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
<Button color="secondary" href="javascript:history.back()" className="ml-2">
|
||||||
Go back
|
Go back
|
||||||
|
|||||||
+38
-43
@@ -1,6 +1,5 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
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 { useEffect } from "react";
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||||
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import defaultEvents, {
|
import defaultEvents, {
|
||||||
@@ -26,9 +26,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
|
|||||||
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 { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
|
import { Icon, HeadSeo } from "@calcom/ui";
|
||||||
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ import { AvatarSSR } from "@components/ui/AvatarSSR";
|
|||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const HeadSeo = dynamic(() => import("@components/seo/head-seo"));
|
|
||||||
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 } = 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
|
||||||
@@ -59,27 +57,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
{eventTypes.map((type, index) => (
|
{eventTypes.map((type, index) => (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className="dark:bg-darkgray-100 dark:border-darkgray-200 group relative rounded-sm border border-neutral-200 bg-white dark:hover:border-neutral-600">
|
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
|
||||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
<Link
|
||||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||||
<div className="flex-shrink">
|
className="flex justify-between px-6 py-4"
|
||||||
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p>
|
data-testid="event-type-link">
|
||||||
<EventTypeDescription className="text-sm" eventType={type} />
|
<div className="flex-shrink">
|
||||||
</div>
|
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p>
|
||||||
<div className="mt-1 self-center">
|
<EventTypeDescription className="text-sm" eventType={type} />
|
||||||
<AvatarGroup
|
</div>
|
||||||
border="border-2 border-white"
|
<div className="mt-1 self-center">
|
||||||
truncateAfter={4}
|
<AvatarGroup
|
||||||
className="flex flex-shrink-0"
|
border="border-2 border-white dark:border-darkgray-100"
|
||||||
size={10}
|
truncateAfter={4}
|
||||||
items={props.users.map((user) => ({
|
className="flex flex-shrink-0"
|
||||||
alt: user.name || "",
|
size={10}
|
||||||
image: user.avatar || "",
|
items={props.users.map((user) => ({
|
||||||
}))}
|
alt: user.name || "",
|
||||||
/>
|
image: user.avatar || "",
|
||||||
</div>
|
}))}
|
||||||
</a>
|
/>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -92,7 +91,6 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
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)
|
||||||
useExposePlanGlobally("PRO");
|
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
|
||||||
@@ -140,7 +138,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white">
|
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white">
|
||||||
{nameOrUsername}
|
{nameOrUsername}
|
||||||
{user.verified && (
|
{user.verified && (
|
||||||
<BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
|
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="dark:text-darkgray-600 text-s text-neutral-500">{user.bio}</p>
|
<p className="dark:text-darkgray-600 text-s text-neutral-500">{user.bio}</p>
|
||||||
@@ -177,22 +175,20 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
href={{
|
href={{
|
||||||
pathname: `/${user.username}/${type.slug}`,
|
pathname: `/${user.username}/${type.slug}`,
|
||||||
query,
|
query,
|
||||||
}}>
|
}}
|
||||||
<a
|
onClick={async () => {
|
||||||
onClick={async () => {
|
sdkActionManager?.fire("eventTypeSelected", {
|
||||||
sdkActionManager?.fire("eventTypeSelected", {
|
eventType: type,
|
||||||
eventType: type,
|
});
|
||||||
});
|
}}
|
||||||
}}
|
className="block w-full p-5"
|
||||||
className="block w-full p-5"
|
data-testid="event-type-link">
|
||||||
data-testid="event-type-link">
|
<div className="flex flex-wrap items-center">
|
||||||
<div className="flex flex-wrap items-center">
|
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
{type.title}
|
||||||
{type.title}
|
</h2>
|
||||||
</h2>
|
</div>
|
||||||
</div>
|
<EventTypeDescription eventType={type} />
|
||||||
<EventTypeDescription eventType={type} />
|
|
||||||
</a>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -272,7 +268,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
away: true,
|
away: true,
|
||||||
verified: true,
|
verified: true,
|
||||||
allowDynamicBooking: true,
|
allowDynamicBooking: true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { JSONObject } from "superjson/dist/types";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||||
@@ -72,7 +72,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
away: true,
|
away: true,
|
||||||
plan: true,
|
|
||||||
name: true,
|
name: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
@@ -130,7 +129,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
const [eventType]: (typeof user.eventTypes[number] & {
|
const [eventType]: (typeof user.eventTypes[number] & {
|
||||||
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
|
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
|
||||||
})[] = [
|
})[] = [
|
||||||
{
|
{
|
||||||
...user.eventTypes[0],
|
...user.eventTypes[0],
|
||||||
@@ -139,7 +138,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -159,9 +157,10 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
// Check if the user you are logging into has any active teams
|
// Check if the user you are logging into has any active teams
|
||||||
const hasActiveTeam =
|
const hasActiveTeam =
|
||||||
user.teams.filter((m) => {
|
user.teams.filter((m) => {
|
||||||
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
if (metadata.success && metadata.data?.subscriptionId) return false;
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||||
return true;
|
return false;
|
||||||
}).length > 0;
|
}).length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -226,7 +225,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: true,
|
theme: true,
|
||||||
plan: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,7 +244,6 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
timeZone: user.timeZone,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
import { DefaultSeo } from "next-seo";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import Script from "next/script";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
@@ -18,7 +19,6 @@ import { seoConfig } from "@lib/config/next-seo.config";
|
|||||||
|
|
||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
import "../styles/fonts.css";
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
function MyApp(props: AppProps) {
|
function MyApp(props: AppProps) {
|
||||||
@@ -31,13 +31,15 @@ function MyApp(props: AppProps) {
|
|||||||
}
|
}
|
||||||
// Use the layout defined at the page level, if available
|
// Use the layout defined at the page level, if available
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProviders {...props}>
|
<AppProviders {...props}>
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
<I18nLanguageHandler />
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
<Head>
|
<Head>
|
||||||
<script dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }} />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
{getLayout(
|
{getLayout(
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import { getDirFromLang } from "@calcom/lib/i18n";
|
||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
|
||||||
@@ -46,11 +49,10 @@ class MyDocument extends Document<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { locale } = this.props.__NEXT_DATA__;
|
const { locale } = this.props.__NEXT_DATA__;
|
||||||
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
|
const dir = getDirFromLang(locale);
|
||||||
return (
|
return (
|
||||||
<Html lang={locale} dir={dir}>
|
<Html lang={locale} dir={dir}>
|
||||||
<Head>
|
<Head>
|
||||||
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
|
||||||
<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" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
@@ -69,7 +71,9 @@ class MyDocument extends Document<Props> {
|
|||||||
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
||||||
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
||||||
*/}
|
*/}
|
||||||
<script
|
<Script
|
||||||
|
id="run-before-client"
|
||||||
|
strategy="beforeInteractive"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
|||||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||||
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
import rateLimit from "@calcom/lib/rateLimit";
|
import rateLimit from "@calcom/lib/rateLimit";
|
||||||
@@ -125,9 +125,10 @@ const providers: Provider[] = [
|
|||||||
// Check if the user you are logging into has any active teams
|
// Check if the user you are logging into has any active teams
|
||||||
const hasActiveTeams =
|
const hasActiveTeams =
|
||||||
user.teams.filter((m) => {
|
user.teams.filter((m) => {
|
||||||
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
||||||
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
||||||
if (metadata.success && metadata.data?.subscriptionId) return false;
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
||||||
return true;
|
return false;
|
||||||
}).length > 0;
|
}).length > 0;
|
||||||
|
|
||||||
// authentication success- but does it meet the minimum password requirements?
|
// authentication success- but does it meet the minimum password requirements?
|
||||||
@@ -137,7 +138,7 @@ const providers: Provider[] = [
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
role: "USER",
|
role: "INACTIVE_ADMIN",
|
||||||
belongsToActiveTeam: hasActiveTeams,
|
belongsToActiveTeam: hasActiveTeams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ async function handler(req: NextApiRequest) {
|
|||||||
name: parsedQuery.data.full_name,
|
name: parsedQuery.data.full_name,
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
locale: "en", // TODO: We should revisit this
|
locale: "en", // TODO: We should revisit this
|
||||||
plan: "PRO",
|
|
||||||
identityProvider: IdentityProvider.CAL,
|
identityProvider: IdentityProvider.CAL,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
|
|
||||||
import { TRIAL_LIMIT_DAYS } from "@lib/config/constants";
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||||
if (process.env.CRON_API_KEY !== apiKey) {
|
if (process.env.CRON_API_KEY !== apiKey) {
|
||||||
@@ -18,35 +13,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* We should add and extra check for non-paying customers in Stripe so we can
|
* Remove this endpoint
|
||||||
* downgrade them here.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
await prisma.user.updateMany({
|
|
||||||
data: {
|
|
||||||
plan: "FREE",
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
plan: "TRIAL",
|
|
||||||
OR: [
|
|
||||||
/**
|
|
||||||
* If the user doesn't have a trial end date,
|
|
||||||
* use the default 14 day trial from creation.
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
createdDate: {
|
|
||||||
lt: dayjs().subtract(TRIAL_LIMIT_DAYS, "day").toDate(),
|
|
||||||
},
|
|
||||||
trialEndsAt: null,
|
|
||||||
},
|
|
||||||
/** If it does, then honor the trial end date. */
|
|
||||||
{
|
|
||||||
trialEndsAt: {
|
|
||||||
lt: dayjs().toDate(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { fetcher } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
|
const getAccessLinkSchema = z.union([
|
||||||
|
z.object({
|
||||||
|
download_link: z.string(),
|
||||||
|
expires: z.number(),
|
||||||
|
}),
|
||||||
|
z.object({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const requestQuery = z.object({
|
||||||
|
recordingId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<z.infer<typeof getAccessLinkSchema> | void>
|
||||||
|
) {
|
||||||
|
const { recordingId } = requestQuery.parse(req.query);
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
// Check if user belong to active team
|
||||||
|
if (!session?.user?.belongsToActiveTeam) {
|
||||||
|
return res.status(403);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetcher(`/recordings/${recordingId}/access-link`).then(getAccessLinkSchema.parse);
|
||||||
|
|
||||||
|
if ("download_link" in response && response.download_link) {
|
||||||
|
return res.status(200).json(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||||
|
});
|
||||||
+21
-21
@@ -13,10 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
title: "30min between Pro Example and pro@example.com",
|
title: "30min between Pro Example and pro@example.com",
|
||||||
description: null,
|
description: null,
|
||||||
additionalNotes: "asdasdas",
|
additionalNotes: "asdasdas",
|
||||||
customInputs: {
|
|
||||||
"Custom input 01": "sadasdasdsadasd",
|
|
||||||
"Custom input 02": "asdasdasd",
|
|
||||||
},
|
|
||||||
startTime: "2022-06-03T09:00:00-06:00",
|
startTime: "2022-06-03T09:00:00-06:00",
|
||||||
endTime: "2022-06-03T09:30:00-06:00",
|
endTime: "2022-06-03T09:30:00-06:00",
|
||||||
organizer: {
|
organizer: {
|
||||||
@@ -32,27 +28,31 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
timeZone: "America/Chihuahua",
|
timeZone: "America/Chihuahua",
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
email: "pro@example.com",
|
|
||||||
name: "pro@example.com",
|
|
||||||
timeZone: "America/Chihuahua",
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: "pro@example.com",
|
|
||||||
name: "pro@example.com",
|
|
||||||
timeZone: "America/Chihuahua",
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
location: "Zoom video",
|
location: "Zoom video",
|
||||||
destinationCalendar: null,
|
destinationCalendar: null,
|
||||||
hideCalendarNotes: false,
|
hideCalendarNotes: false,
|
||||||
uid: "bwPWLpjYrx4rZf6MCZdKgE",
|
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||||
metadata: {},
|
metadata: {},
|
||||||
cancellationReason: "It got late",
|
|
||||||
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
|
|
||||||
recurringEvent: null,
|
recurringEvent: null,
|
||||||
|
appsStatus: [
|
||||||
|
{
|
||||||
|
appName: "Outlook Calendar",
|
||||||
|
type: "office365_calendar",
|
||||||
|
success: 1,
|
||||||
|
failures: 0,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: "Google Meet",
|
||||||
|
type: "conferencing",
|
||||||
|
success: 0,
|
||||||
|
failures: 1,
|
||||||
|
errors: [],
|
||||||
|
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
req.statusCode = 200;
|
req.statusCode = 200;
|
||||||
@@ -60,9 +60,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("AttendeeScheduledEmail", {
|
renderEmail("OrganizerScheduledEmail", {
|
||||||
attendee: evt.attendees[0],
|
|
||||||
calEvent: evt,
|
calEvent: evt,
|
||||||
|
attendee: evt.organizer,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
|
||||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||||
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 { getSession } from "@lib/auth";
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
const defaultIntegrationAddHandler = async ({
|
const defaultIntegrationAddHandler = async ({
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
|
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
||||||
|
|
||||||
|
enum DirectAction {
|
||||||
|
ACCEPT = "accept",
|
||||||
|
REJECT = "reject",
|
||||||
|
}
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
action: z.nativeEnum(DirectAction),
|
||||||
|
token: z.string(),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedSchema = z.object({
|
||||||
|
bookingUid: z.string(),
|
||||||
|
userId: z.number().int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||||
|
const { action, token, reason } = querySchema.parse(req.query);
|
||||||
|
const { bookingUid, userId } = decryptedSchema.parse(
|
||||||
|
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({
|
||||||
|
where: { uid: bookingUid },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @see https://trpc.io/docs/server-side-calls */
|
||||||
|
const ctx = await createContext({ req, res }, sessionGetter);
|
||||||
|
const caller = viewerRouter.createCaller(ctx);
|
||||||
|
await caller.bookings.confirm({
|
||||||
|
bookingId: booking.id,
|
||||||
|
recurringEventId: booking.recurringEventId || undefined,
|
||||||
|
confirmed: action === DirectAction.ACCEPT,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
let message = "Error confirming booking";
|
||||||
|
if (e instanceof TRPCError) message = e.message;
|
||||||
|
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(`/booking/${bookingUid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultResponder(handler);
|
||||||
@@ -53,6 +53,8 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
interFontMedium,
|
interFontMedium,
|
||||||
]);
|
]);
|
||||||
const ogConfig = {
|
const ogConfig = {
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
fonts: [
|
fonts: [
|
||||||
{ name: "inter", data: interFontData, weight: 400 },
|
{ name: "inter", data: interFontData, weight: 400 },
|
||||||
{ name: "inter", data: interFontMediumData, weight: 500 },
|
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||||
@@ -71,7 +73,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
meetingImage: searchParams.get("meetingImage"),
|
meetingImage: searchParams.get("meetingImage"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
return new ImageResponse(
|
const img = new ImageResponse(
|
||||||
(
|
(
|
||||||
<Meeting
|
<Meeting
|
||||||
title={title}
|
title={title}
|
||||||
@@ -80,7 +82,9 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
ogConfig
|
ogConfig
|
||||||
);
|
) as { body: Buffer };
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
case "app": {
|
case "app": {
|
||||||
const { name, description, slug } = appSchema.parse({
|
const { name, description, slug } = appSchema.parse({
|
||||||
@@ -89,7 +93,11 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
slug: searchParams.get("slug"),
|
slug: searchParams.get("slug"),
|
||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
|
||||||
|
body: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "generic": {
|
case "generic": {
|
||||||
@@ -99,7 +107,11 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
imageType,
|
imageType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||||
|
body: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Response(img.body, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
select: {
|
select: {
|
||||||
username: true,
|
username: true,
|
||||||
id: true,
|
id: true,
|
||||||
plan: true,
|
|
||||||
createdDate: true,
|
createdDate: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -81,7 +80,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
? new Date(lastBooking.booking.createdAt).toLocaleDateString("en-US")
|
||||||
: "No info"
|
: "No info"
|
||||||
}</li>
|
}</li>
|
||||||
<li><b>Plan:</b> ${user.plan}</li>
|
|
||||||
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
<li><b>Account created:</b> ${new Date(user.createdDate).toLocaleDateString("en-US")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -1,86 +1,72 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { defaultAvatarSrc } from "@lib/profile";
|
import { defaultAvatarSrc } from "@lib/profile";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
const querySchema = z
|
||||||
// const username = req.url?.substring(1, req.url.lastIndexOf("/"));
|
.object({
|
||||||
const username = req.query.username as string;
|
username: z.string(),
|
||||||
const teamname = req.query.teamname as string;
|
teamname: z.string(),
|
||||||
let identity;
|
})
|
||||||
let linksToThisRoute = false;
|
.partial();
|
||||||
|
|
||||||
|
async function getIdentityData(req: NextApiRequest) {
|
||||||
|
const { username, teamname } = querySchema.parse(req.query);
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: { username },
|
||||||
username: username,
|
select: { avatar: true, email: true },
|
||||||
},
|
|
||||||
select: {
|
|
||||||
avatar: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
identity = {
|
return {
|
||||||
name: username,
|
name: username,
|
||||||
email: user?.email,
|
email: user?.email,
|
||||||
avatar: user?.avatar,
|
avatar: user?.avatar,
|
||||||
};
|
};
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
|
|
||||||
} else if (teamname) {
|
|
||||||
const team = await prisma.team.findUnique({
|
|
||||||
where: {
|
|
||||||
slug: teamname,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
logo: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
identity = {
|
|
||||||
name: teamname,
|
|
||||||
shouldDefaultBeNameBased: true,
|
|
||||||
avatar: team?.logo,
|
|
||||||
};
|
|
||||||
linksToThisRoute =
|
|
||||||
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
|
|
||||||
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
|
|
||||||
}
|
}
|
||||||
|
if (teamname) {
|
||||||
const emailMd5 = crypto
|
const team = await prisma.team.findUnique({
|
||||||
.createHash("md5")
|
where: { slug: teamname },
|
||||||
.update((identity?.email as string) || "guest@example.com")
|
select: { logo: true },
|
||||||
.digest("hex");
|
|
||||||
const img = identity?.avatar;
|
|
||||||
// If image isn't set or links to this route itself, use default avatar
|
|
||||||
if (!img || linksToThisRoute) {
|
|
||||||
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
|
||||||
if (identity?.shouldDefaultBeNameBased) {
|
|
||||||
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
|
||||||
}
|
|
||||||
res.writeHead(302, {
|
|
||||||
Location: defaultSrc,
|
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
res.end();
|
name: teamname,
|
||||||
} else if (!img.includes("data:image")) {
|
email: null,
|
||||||
res.writeHead(302, {
|
avatar: team?.logo || getPlaceholderAvatar(null, teamname),
|
||||||
Location: img,
|
};
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
} else {
|
|
||||||
const decoded = img
|
|
||||||
.toString()
|
|
||||||
.replace("data:image/png;base64,", "")
|
|
||||||
.replace("data:image/jpeg;base64,", "");
|
|
||||||
const imageResp = Buffer.from(decoded, "base64");
|
|
||||||
res.writeHead(200, {
|
|
||||||
"Content-Type": "image/png",
|
|
||||||
"Content-Length": imageResp.length,
|
|
||||||
});
|
|
||||||
res.end(imageResp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const identity = await getIdentityData(req);
|
||||||
|
const img = identity?.avatar;
|
||||||
|
// If image isn't set or links to this route itself, use default avatar
|
||||||
|
if (!img) {
|
||||||
|
res.writeHead(302, {
|
||||||
|
Location: defaultAvatarSrc({
|
||||||
|
md5: crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(identity?.email || "guest@example.com")
|
||||||
|
.digest("hex"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!img.includes("data:image")) {
|
||||||
|
res.writeHead(302, { Location: img });
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
|
||||||
|
const imageResp = Buffer.from(decoded, "base64");
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": "image/png",
|
||||||
|
"Content-Length": imageResp.length,
|
||||||
|
});
|
||||||
|
res.end(imageResp);
|
||||||
|
}
|
||||||
@@ -1,34 +1,18 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import { MDXRemote } from "next-mdx-remote";
|
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
|
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import App from "@components/apps/App";
|
import App from "@components/apps/App";
|
||||||
|
|
||||||
const components = {
|
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||||
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
|
|
||||||
<Link href={href}>
|
|
||||||
<a {...otherProps} />
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
img: ({ src = "", alt = "", placeholder, ...rest }: JSX.IntrinsicElements["img"]) => (
|
|
||||||
<Image src={src} alt={alt} {...rest} />
|
|
||||||
),
|
|
||||||
// @TODO: In v2 the slider isn't shown anymore. However, to ensure the v1 pages keep
|
|
||||||
// working, this component is still rendered in the MDX content. To skip them in the v2
|
|
||||||
// content we have to render null here. In v2 the gallery is shown by directly
|
|
||||||
// using the `items` property from the MDX's meta data.
|
|
||||||
Slider: () => <></>,
|
|
||||||
};
|
|
||||||
|
|
||||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
@@ -50,10 +34,15 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
email={data.email}
|
email={data.email}
|
||||||
licenseRequired={data.licenseRequired}
|
licenseRequired={data.licenseRequired}
|
||||||
isProOnly={data.isProOnly}
|
isProOnly={data.isProOnly}
|
||||||
images={source?.scope?.items as string[] | undefined}
|
images={source.data?.items as string[] | undefined}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
// privacy="https://zoom.us/privacy"
|
// privacy="https://zoom.us/privacy"
|
||||||
body={<MDXRemote {...source} components={components} />}
|
body={
|
||||||
|
<>
|
||||||
|
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,7 +71,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
if (!singleApp) return { notFound: true };
|
if (!singleApp) return { notFound: true };
|
||||||
|
|
||||||
const appDirname = app.dirName;
|
const appDirname = app.dirName;
|
||||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||||
const postFilePath = path.join(README_PATH);
|
const postFilePath = path.join(README_PATH);
|
||||||
let source = "";
|
let source = "";
|
||||||
|
|
||||||
@@ -90,16 +79,15 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
/* If the app doesn't have a README we fallback to the package description */
|
/* If the app doesn't have a README we fallback to the package description */
|
||||||
source = fs.readFileSync(postFilePath).toString();
|
source = fs.readFileSync(postFilePath).toString();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`No README.mdx provided for: ${appDirname}`);
|
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||||
source = singleApp.description;
|
source = singleApp.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { content, data } = matter(source);
|
const { content, data } = matter(source);
|
||||||
const mdxSource = await serialize(content, { scope: data });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
source: mdxSource,
|
source: { content, data },
|
||||||
data: singleApp,
|
data: singleApp,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
backPath="/apps"
|
backPath="/apps"
|
||||||
heading={
|
heading={
|
||||||
<>
|
<>
|
||||||
<Link href="/apps">
|
<Link
|
||||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
href="/apps"
|
||||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||||
</a>
|
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
{category && (
|
{category && (
|
||||||
<span className="gap-1 text-gray-600">
|
<span className="gap-1 text-gray-600">
|
||||||
@@ -47,13 +47,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const paths = Object.keys(AppCategories);
|
||||||
const paths = appStore.reduce((categories, app) => {
|
|
||||||
if (!categories.includes(app.category)) {
|
|
||||||
categories.push(app.category);
|
|
||||||
}
|
|
||||||
return categories;
|
|
||||||
}, [] as string[]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: paths.map((category) => ({ params: { category } })),
|
paths: paths.map((category) => ({ params: { category } })),
|
||||||
|
|||||||
@@ -11,28 +11,28 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
return (
|
return (
|
||||||
<Shell isPublic large>
|
<Shell isPublic large>
|
||||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||||
<Link href="/apps">
|
<Link
|
||||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
href="/apps"
|
||||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||||
</a>
|
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
<Link
|
||||||
<a
|
key={category.name}
|
||||||
data-testid={`app-store-category-${category.name}`}
|
href={"/apps/categories/" + category.name}
|
||||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
data-testid={`app-store-category-${category.name}`}
|
||||||
<div className="self-center">
|
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
<div className="self-center">
|
||||||
<p className="text-sm text-gray-500">
|
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||||
{t("number_apps", { count: category.count })}{" "}
|
<p className="text-sm text-gray-500">
|
||||||
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
{t("number_apps", { count: category.count })}{" "}
|
||||||
</p>
|
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
||||||
</div>
|
</p>
|
||||||
</a>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,9 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const categories = appStore.reduce((c, app) => {
|
const categories = appStore.reduce((c, app) => {
|
||||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
for (const category of app.categories) {
|
||||||
|
c[category] = c[category] ? c[category] + 1 : 1;
|
||||||
|
}
|
||||||
return c;
|
return c;
|
||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { InferGetStaticPropsType, NextPageContext } from "next";
|
|
||||||
import { ChangeEventHandler, useState } from "react";
|
import { ChangeEventHandler, useState } from "react";
|
||||||
|
|
||||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||||
@@ -7,7 +6,10 @@ import { classNames } from "@calcom/lib";
|
|||||||
import { getSession } from "@calcom/lib/auth";
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { AppCategories } from "@calcom/prisma/client";
|
import type { AppCategories } from "@calcom/prisma/client";
|
||||||
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
||||||
|
|
||||||
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ function AppsSearch({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
@@ -42,19 +44,24 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
|||||||
subtitle={t("app_store_description")}
|
subtitle={t("app_store_description")}
|
||||||
actions={(className) => (
|
actions={(className) => (
|
||||||
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
<AppsSearch className={className} onChange={(e) => setSearchText(e.target.value)} />
|
||||||
)}>
|
)}
|
||||||
|
emptyStore={!appStore.length}>
|
||||||
{!searchText && (
|
{!searchText && (
|
||||||
<>
|
<>
|
||||||
<AppStoreCategories categories={categories} />
|
<AppStoreCategories categories={categories} />
|
||||||
<TrendingAppsSlider items={appStore} />
|
<TrendingAppsSlider items={appStore} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<AllApps apps={appStore} searchText={searchText} />
|
<AllApps
|
||||||
|
apps={appStore}
|
||||||
|
searchText={searchText}
|
||||||
|
categories={categories.map((category) => category.name)}
|
||||||
|
/>
|
||||||
</AppsLayout>
|
</AppsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: NextPageContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const ssg = await ssgInit(context);
|
const ssg = await ssgInit(context);
|
||||||
|
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
@@ -77,7 +84,6 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
trpcState: ssg.dehydrate(),
|
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
name: name as AppCategories,
|
name: name as AppCategories,
|
||||||
@@ -87,6 +93,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -14,16 +14,16 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
EmptyScreen,
|
EmptyScreen,
|
||||||
Icon,
|
Icon,
|
||||||
InstalledAppsLayout,
|
|
||||||
List,
|
List,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
SkeletonLoader,
|
AppSkeletonLoader as SkeletonLoader,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||||
|
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||||
|
|
||||||
function ConnectOrDisconnectIntegrationButton(props: {
|
function ConnectOrDisconnectIntegrationButton(props: {
|
||||||
credentialIds: number[];
|
credentialIds: number[];
|
||||||
@@ -32,7 +32,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
installed?: boolean;
|
installed?: boolean;
|
||||||
invalidCredentialIds?: number[];
|
invalidCredentialIds?: number[];
|
||||||
}) {
|
}) {
|
||||||
const { type, credentialIds, isGlobal, installed, invalidCredentialIds } = props;
|
const { type, credentialIds, isGlobal, installed } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [credentialId] = credentialIds;
|
const [credentialId] = credentialIds;
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
|||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
data-testid={`connect-${variant || "other"}-apps`}
|
||||||
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
||||||
{t(`connect_${variant || "other"}_apps`)}
|
{t(`connect_${variant || "other"}_apps`)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function Error() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<Link href="/auth/login" passHref>
|
<Link href="/auth/login" passHref legacyBehavior>
|
||||||
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
|||||||
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
|
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<p>{t("request_is_expired_instructions")}</p>
|
<p>{t("request_is_expired_instructions")}</p>
|
||||||
<Link href="/auth/forgot-password" passHref>
|
<Link href="/auth/forgot-password" passHref legacyBehavior>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
|||||||
footerText={
|
footerText={
|
||||||
!success && (
|
!success && (
|
||||||
<>
|
<>
|
||||||
<Link href="/auth/login">
|
<Link href="/auth/login" className="font-medium text-neutral-900">
|
||||||
<a className="font-medium text-neutral-900">{t("back_to_signin")}</a>
|
{t("back_to_signin")}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { useState } from "react";
|
|||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { FaGoogle } from "react-icons/fa";
|
import { FaGoogle } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Alert, Button, EmailField, Icon, PasswordField, SAMLLogin } from "@calcom/ui";
|
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
|
||||||
|
|
||||||
import { ErrorCode, getSession } from "@lib/auth";
|
import { ErrorCode, getSession } from "@lib/auth";
|
||||||
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
||||||
@@ -131,11 +132,12 @@ export default function Login({
|
|||||||
{...register("email")}
|
{...register("email")}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute right-0 -top-[6px] z-10">
|
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
|
||||||
<Link href="/auth/forgot-password">
|
<Link
|
||||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
href="/auth/forgot-password"
|
||||||
{t("forgot")}
|
tabIndex={-1}
|
||||||
</a>
|
className="text-sm font-medium text-gray-600">
|
||||||
|
{t("forgot")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<PasswordField
|
<PasswordField
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { UserPermissionRole } from "@prisma/client";
|
||||||
|
import { GetServerSidePropsContext } from "next";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
|
import { getSession } from "@calcom/lib/auth";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
import { WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
|
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||||
|
import StepDone from "./steps/StepDone";
|
||||||
|
|
||||||
|
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||||
|
const shouldDisable = props.userCount !== 0;
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: t("administrator_user"),
|
||||||
|
description: t("lets_create_first_administrator_user"),
|
||||||
|
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||||
|
isLoading: isLoadingStep1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("enable_apps"),
|
||||||
|
description: t("enable_apps_description"),
|
||||||
|
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex items-center bg-gray-100 print:h-full">
|
||||||
|
<WizardForm
|
||||||
|
href="/auth/setup"
|
||||||
|
steps={steps}
|
||||||
|
nextLabel={t("next_step_text")}
|
||||||
|
finishLabel={t("finish")}
|
||||||
|
prevLabel={t("prev_step")}
|
||||||
|
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
const { req } = context;
|
||||||
|
const session = await getSession({ req });
|
||||||
|
|
||||||
|
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/404`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
userCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
+13
-60
@@ -1,34 +1,14 @@
|
|||||||
import { CheckIcon } from "@heroicons/react/solid";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
|
|
||||||
import { isPasswordValid } from "@calcom/lib/auth";
|
import { isPasswordValid } from "@calcom/lib/auth";
|
||||||
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|
||||||
import { EmailField, Label, PasswordField, TextField, WizardForm } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
|
||||||
|
|
||||||
const StepDone = () => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
|
||||||
<CheckIcon className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
|
||||||
</div>
|
|
||||||
<div className="max-w-[420px] text-center">
|
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -83,13 +63,19 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
router.replace(`/auth/login?email=${data.email_address.toLowerCase()}`);
|
await signIn("credentials", {
|
||||||
|
redirect: false,
|
||||||
|
callbackUrl: "/",
|
||||||
|
email: data.email_address.toLowerCase(),
|
||||||
|
password: data.password,
|
||||||
|
});
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
} else {
|
} else {
|
||||||
router.replace("/auth/setup");
|
router.replace("/auth/setup");
|
||||||
}
|
}
|
||||||
}, onError);
|
}, onError);
|
||||||
|
|
||||||
const longWebsiteUrl = process.env.NEXT_PUBLIC_WEBSITE_URL.length > 30;
|
const longWebsiteUrl = WEBSITE_URL.length > 30;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
@@ -111,7 +97,7 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
<TextField
|
<TextField
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
!longWebsiteUrl && (
|
!longWebsiteUrl && (
|
||||||
<span className="items-centerpx-3 inline-flex rounded-none text-sm text-gray-500">
|
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500">
|
||||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@@ -201,37 +187,4 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
export default SetupFormStep1;
|
||||||
const { t } = useLocale();
|
|
||||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
|
||||||
|
|
||||||
const steps = [
|
|
||||||
{
|
|
||||||
title: t("administrator_user"),
|
|
||||||
description: t("lets_create_first_administrator_user"),
|
|
||||||
content: props.userCount !== 0 ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
|
||||||
enabled: props.userCount === 0, // to check if the wizard should show buttons to navigate through more steps
|
|
||||||
isLoading: isLoadingStep1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className="flex h-screen items-center bg-gray-100 print:h-full">
|
|
||||||
<WizardForm href="/auth/setup" steps={steps} containerClassname="max-w-sm" />
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
userCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
const StepDone = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
id="wizard-step-1"
|
||||||
|
name="wizard-step-1"
|
||||||
|
className="space-y-4"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||||
|
}}>
|
||||||
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
|
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||||
|
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div className="max-w-[420px] text-center">
|
||||||
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepDone;
|
||||||
@@ -6,7 +6,7 @@ import * as React from "react";
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export default function Verify() {
|
|||||||
? "Your payment failed"
|
? "Your payment failed"
|
||||||
: sessionId
|
: sessionId
|
||||||
? "Payment successful!"
|
? "Payment successful!"
|
||||||
: "Verify your email" + " | Cal.com"}
|
: "Verify your email" + " | " + APP_NAME}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { GetStaticPaths, GetStaticProps } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useEffect } from "react";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||||
import Schedule from "@calcom/features/schedules/components/Schedule";
|
import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
import { availabilityAsString } from "@calcom/lib/availability";
|
||||||
|
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
||||||
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 type { Schedule as ScheduleType } from "@calcom/types/schedule";
|
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
SkeletonText,
|
SkeletonText,
|
||||||
Switch,
|
Switch,
|
||||||
TimezoneSelect,
|
TimezoneSelect,
|
||||||
|
Tooltip,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ import { HttpError } from "@lib/core/http/error";
|
|||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
schedule: stringOrNumber,
|
schedule: stringOrNumber,
|
||||||
@@ -38,31 +40,59 @@ const querySchema = z.object({
|
|||||||
type AvailabilityFormValues = {
|
type AvailabilityFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
schedule: ScheduleType;
|
schedule: ScheduleType;
|
||||||
|
dateOverrides: { ranges: TimeRange[] }[];
|
||||||
timeZone: string;
|
timeZone: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||||
|
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||||
|
name: "dateOverrides",
|
||||||
|
});
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="font-medium leading-6 text-gray-900">
|
||||||
|
{t("date_overrides")}{" "}
|
||||||
|
<Tooltip content={t("date_overrides_info")}>
|
||||||
|
<span className="inline-block">
|
||||||
|
<Icon.FiInfo />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</h3>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
||||||
|
<div className="mt-1 space-y-2">
|
||||||
|
<DateOverrideList
|
||||||
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
|
remove={remove}
|
||||||
|
update={update}
|
||||||
|
items={fields}
|
||||||
|
workingHours={workingHours}
|
||||||
|
/>
|
||||||
|
<DateOverrideInputDialog
|
||||||
|
workingHours={workingHours}
|
||||||
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
|
onChange={(ranges) => append({ ranges })}
|
||||||
|
Trigger={
|
||||||
|
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
|
||||||
|
Add an override
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Availability({ schedule }: { schedule: number }) {
|
export default function Availability({ schedule }: { schedule: number }) {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const me = useMeQuery();
|
const me = useMeQuery();
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
|
||||||
|
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
|
||||||
const form = useForm<AvailabilityFormValues>();
|
const form = useForm<AvailabilityFormValues>({ defaultValues });
|
||||||
const { control, reset } = form;
|
const { control } = form;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading && data) {
|
|
||||||
reset({
|
|
||||||
name: data?.schedule?.name,
|
|
||||||
schedule: data.availability,
|
|
||||||
timeZone: data.timeZone,
|
|
||||||
isDefault: data.isDefault,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data, isLoading, reset]);
|
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
|
||||||
if (prevDefaultId && currentDefaultId) {
|
if (prevDefaultId && currentDefaultId) {
|
||||||
@@ -73,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
|
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
|
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
|
||||||
utils.viewer.availability.list.invalidate();
|
utils.viewer.availability.list.invalidate();
|
||||||
showToast(
|
showToast(
|
||||||
t("availability_updated_successfully", {
|
t("availability_updated_successfully", {
|
||||||
@@ -98,17 +128,21 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => <EditableHeading isReady={!isLoading} {...field} />}
|
render={({ field }) => (
|
||||||
|
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
subtitle={
|
subtitle={
|
||||||
data ? (
|
data ? (
|
||||||
data.schedule.availability.map((availability) => (
|
data.schedule.availability
|
||||||
<span key={availability.id}>
|
.filter((availability) => !!availability.days.length)
|
||||||
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
|
.map((availability) => (
|
||||||
<br />
|
<span key={availability.id}>
|
||||||
</span>
|
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
|
||||||
))
|
<br />
|
||||||
|
</span>
|
||||||
|
))
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText className="h-4 w-48" />
|
<SkeletonText className="h-4 w-48" />
|
||||||
)
|
)
|
||||||
@@ -140,95 +174,88 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<div className="flex items-baseline sm:mt-0">
|
<div className="w-full">
|
||||||
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
|
<Form
|
||||||
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
|
form={form}
|
||||||
<div className="w-full">
|
id="availability-form"
|
||||||
<Form
|
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||||
form={form}
|
updateMutation.mutate({
|
||||||
id="availability-form"
|
scheduleId: schedule,
|
||||||
handleSubmit={async (values) => {
|
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||||
updateMutation.mutate({
|
...values,
|
||||||
scheduleId: schedule,
|
});
|
||||||
...values,
|
}}
|
||||||
});
|
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||||
}}
|
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
||||||
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
<div className=" py-5 sm:p-6">
|
||||||
<div className="flex-1">
|
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
|
||||||
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
|
{t("change_start_end")}
|
||||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
</h3>
|
||||||
{t("change_start_end")}
|
{typeof me.data?.weekStart === "string" && (
|
||||||
</h3>
|
<Schedule
|
||||||
{typeof me.data?.weekStart === "string" && (
|
control={control}
|
||||||
<Schedule
|
name="schedule"
|
||||||
control={control}
|
weekStart={
|
||||||
name="schedule"
|
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||||
weekStart={
|
me.data?.weekStart
|
||||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||||
me.data?.weekStart
|
}
|
||||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
/>
|
||||||
}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||||
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
</div>
|
||||||
<div>
|
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||||
{t("timezone")}
|
<div>
|
||||||
</label>
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
<Controller
|
{t("timezone")}
|
||||||
name="timeZone"
|
</label>
|
||||||
render={({ field: { onChange, value } }) =>
|
<Controller
|
||||||
value ? (
|
name="timeZone"
|
||||||
<TimezoneSelect
|
render={({ field: { onChange, value } }) =>
|
||||||
value={value}
|
value ? (
|
||||||
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
<TimezoneSelect
|
||||||
onChange={(timezone) => onChange(timezone.value)}
|
value={value}
|
||||||
/>
|
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
||||||
) : (
|
onChange={(timezone) => onChange(timezone.value)}
|
||||||
<SelectSkeletonLoader className="w-72" />
|
/>
|
||||||
)
|
) : (
|
||||||
}
|
<SelectSkeletonLoader className="w-72" />
|
||||||
/>
|
)
|
||||||
</div>
|
}
|
||||||
<hr className="my-8" />
|
/>
|
||||||
<div className="rounded-md">
|
</div>
|
||||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
<hr className="my-8" />
|
||||||
<div className="mt-3 flex">
|
<div className="rounded-md">
|
||||||
<Button href="/availability/troubleshoot" color="secondary">
|
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||||
{t("launch_troubleshooter")}
|
<div className="mt-3 flex">
|
||||||
</Button>
|
<Button href="/availability/troubleshoot" color="secondary">
|
||||||
</div>
|
{t("launch_troubleshooter")}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</div>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
const ssg = await ssgInit(ctx);
|
const ssr = await ssrInit(ctx);
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
if (!params.success) return { notFound: true };
|
||||||
|
|
||||||
|
const scheduleId = params.data.schedule;
|
||||||
|
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
|
||||||
|
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
schedule: params.data.schedule,
|
schedule: scheduleId,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: "blocking",
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Shell, SkeletonText } from "@calcom/ui";
|
import { Shell, SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
type User = RouterOutputs["viewer"]["me"];
|
type User = RouterOutputs["viewer"]["me"];
|
||||||
|
|
||||||
export interface IBusySlot {
|
export interface IBusySlot {
|
||||||
@@ -16,7 +16,9 @@ export interface IBusySlot {
|
|||||||
|
|
||||||
const AvailabilityView = ({ user }: { user: User }) => {
|
const AvailabilityView = ({ user }: { user: User }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
||||||
|
const selectedDate = dayjs(date);
|
||||||
|
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,17 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const overrides =
|
||||||
|
data?.dateOverrides.reduce((acc, override) => {
|
||||||
|
if (
|
||||||
|
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
|
||||||
|
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
|
||||||
|
)
|
||||||
|
return acc;
|
||||||
|
acc.push({ ...override, source: "Date override" });
|
||||||
|
return acc;
|
||||||
|
}, [] as IBusySlot[]) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
@@ -37,9 +50,9 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="inline h-8 border-none p-0"
|
className="inline h-8 border-none p-0"
|
||||||
defaultValue={selectedDate.format("YYYY-MM-DD")}
|
defaultValue={formattedSelectedDate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.value) setSelectedDate(dayjs(e.target.value));
|
if (e.target.value) setSelectedDate(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
||||||
@@ -49,39 +62,47 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
|||||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (
|
{(() => {
|
||||||
<>
|
if (isLoading)
|
||||||
<SkeletonText className="block h-16 w-full" />
|
return (
|
||||||
<SkeletonText className="block h-16 w-full" />
|
<>
|
||||||
</>
|
<SkeletonText className="block h-16 w-full" />
|
||||||
) : data && data.busy.length > 0 ? (
|
<SkeletonText className="block h-16 w-full" />
|
||||||
data.busy
|
</>
|
||||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
);
|
||||||
.map((slot: IBusySlot) => (
|
|
||||||
<div
|
if (data && (data.busy.length > 0 || overrides.length > 0))
|
||||||
key={dayjs(slot.start).format("HH:mm")}
|
return [...data.busy, ...overrides]
|
||||||
className="overflow-hidden rounded-md bg-neutral-100">
|
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||||
<div className="px-4 py-5 text-black sm:p-6">
|
.map((slot: IBusySlot) => (
|
||||||
{t("calendar_shows_busy_between")}{" "}
|
<div
|
||||||
<span className="font-medium text-neutral-800" title={dayjs(slot.start).format("HH:mm")}>
|
key={dayjs(slot.start).format("HH:mm")}
|
||||||
{dayjs(slot.start).format("HH:mm")}
|
className="overflow-hidden rounded-md bg-neutral-100"
|
||||||
</span>{" "}
|
data-testid="troubleshooter-busy-time">
|
||||||
{t("and")}{" "}
|
<div className="px-4 py-5 text-black sm:p-6">
|
||||||
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
|
{t("calendar_shows_busy_between")}{" "}
|
||||||
{dayjs(slot.end).format("HH:mm")}
|
<span
|
||||||
</span>{" "}
|
className="font-medium text-neutral-800"
|
||||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
title={dayjs(slot.start).format("HH:mm")}>
|
||||||
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
{dayjs(slot.start).format("HH:mm")}
|
||||||
{slot.title && ` - (${slot.title})`}
|
</span>{" "}
|
||||||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
{t("and")}{" "}
|
||||||
|
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
|
||||||
|
{dayjs(slot.end).format("HH:mm")}
|
||||||
|
</span>{" "}
|
||||||
|
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
||||||
|
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
||||||
|
{slot.title && ` - (${slot.title})`}
|
||||||
|
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
));
|
||||||
))
|
return (
|
||||||
) : (
|
<div className="overflow-hidden rounded-md bg-neutral-100">
|
||||||
<div className="overflow-hidden rounded-md bg-neutral-100">
|
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)}
|
})()}
|
||||||
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||||
|
|||||||
Loaded 100 of 778 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user