Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4cc7c766 | ||
|
|
648886f630 | ||
|
|
8d7b2cfe51 | ||
|
|
bfc2b8df34 | ||
|
|
a36f2aa871 | ||
|
|
6d8a576f60 | ||
|
|
b5e54d6cf9 | ||
|
|
ea20fa9f35 | ||
|
|
c3b2a52815 | ||
|
|
6bd97065e6 | ||
|
|
878a7cbfc5 | ||
|
|
9c84c3db0a | ||
|
|
1cf8ef8155 | ||
|
|
14aa0a2f2d | ||
|
|
5d73095060 | ||
|
|
08683a4883 | ||
|
|
2f3cc03255 | ||
|
|
b5fd33d689 | ||
|
|
6799e5f214 | ||
|
|
63e93fb12c | ||
|
|
94289a84f1 | ||
|
|
0f332cc35e | ||
|
|
504d900d05 | ||
|
|
7ebcc08182 | ||
|
|
b137c87634 | ||
|
|
fefb297c47 | ||
|
|
c412e7f815 | ||
|
+2 |
cc99e1697b | ||
|
|
ad4d71d8c0 | ||
|
|
b3226dc308 | ||
|
|
9ef8903309 | ||
|
|
c46c9bfe90 | ||
|
|
583571247c | ||
|
|
05655a92ca | ||
|
|
6fb51db6ac | ||
|
|
407b0518b3 | ||
|
|
8436025ff5 | ||
|
|
4bea5a09c6 | ||
|
|
9b25f81b61 | ||
|
|
0c3ca5c267 | ||
|
|
2f85b8ad6b | ||
|
|
480bd48680 | ||
|
|
0e022b013c | ||
|
|
70b8a38b95 | ||
|
|
94eae71675 | ||
|
|
6f595b16ba | ||
|
|
0871e29f69 | ||
|
|
513a01fc74 | ||
|
|
b35532c07f | ||
|
|
2ae197b66d | ||
|
|
647ba8c19c | ||
|
|
e5ca77eb93 | ||
|
|
44191e79a1 | ||
|
|
be40e4b4b0 | ||
|
|
000f39b9c8 | ||
|
|
4be693341b | ||
|
|
6a96531da5 | ||
|
|
8a6b37104f | ||
|
|
d180e14013 | ||
|
|
44e5002369 | ||
|
|
f1aed2d8b7 | ||
|
|
b0e81e6b7b | ||
|
|
8bbc05b1e5 | ||
|
|
19f76e86d9 | ||
|
|
e731c33235 | ||
|
|
5059723ad1 | ||
|
|
611e109c28 | ||
|
|
3b1cca2505 | ||
|
|
759af31838 | ||
|
|
1e6b4861d9 | ||
|
|
617cf9c538 | ||
|
|
171827f547 | ||
|
|
8cfb9902ff | ||
|
|
79779e5166 | ||
|
|
6a975194bb | ||
|
|
a9f74b68f5 | ||
|
|
15692374c6 | ||
|
|
723b754703 | ||
|
|
bddec9d1de | ||
|
|
42494c7c2b | ||
|
|
852d5b3954 | ||
|
|
5f718bd9f7 | ||
|
|
b9ced0ecf8 | ||
|
|
04ed36cfcf | ||
|
|
467cacb8da | ||
|
|
b799fdcb7e | ||
|
|
d8017e4d2d | ||
|
|
8384c3f9ff | ||
|
|
66d47974cb | ||
|
|
3556dda252 | ||
|
|
c2e91b8378 | ||
|
|
4fe69b5864 | ||
|
|
95ce3b215d | ||
|
|
007f3319a0 | ||
|
|
9441da9d4e | ||
|
|
1510740cb2 | ||
|
|
8da68cc1fe | ||
|
|
6dbc8dd820 | ||
|
|
4ada428bcb | ||
|
|
6adaf0d3e8 | ||
|
|
c9e6acfa69 | ||
|
|
2272da5ee7 | ||
|
|
79b3875af3 | ||
|
|
d16b6fce8f | ||
|
|
40b35bdbe4 | ||
|
|
0af76ec5ab | ||
|
|
f7b20da640 | ||
|
|
eedf6913b8 | ||
|
|
78393a2b8b | ||
|
|
b3d64104b0 | ||
|
|
9266ba1e48 | ||
|
|
ec212e4317 | ||
|
|
02d4f0542a | ||
|
|
0aae07ae2f | ||
|
|
80ab6c9c98 | ||
|
|
86ba0766fc | ||
|
|
0adde17f38 | ||
|
|
9e4700f9df | ||
|
|
f4a87c66b3 | ||
|
|
0c06f1f6cb | ||
|
|
b731dfd421 | ||
|
|
0b704b9ed4 | ||
|
|
a0ece0d77e | ||
|
|
7839381911 | ||
|
|
6a976f5b42 | ||
|
|
8718905474 | ||
|
|
ff968d3867 | ||
|
|
8f5e6370b0 | ||
|
|
0a0c400333 | ||
|
|
0eb05aa540 | ||
|
|
efe18ebdcd | ||
|
|
ce097b89ba | ||
|
|
bb0e6389fa | ||
|
|
19a7cd1a50 | ||
|
|
49bcd5fec7 | ||
|
|
000a6e0d35 | ||
|
|
52936f60b4 | ||
|
|
2d6eebcd8a | ||
|
|
cfa6a5b996 | ||
|
|
7413ba6aa0 | ||
|
|
e9cbe5d353 | ||
|
|
d8bc9acbbc | ||
|
|
4f0e843ac6 | ||
|
|
02f1c50bb2 | ||
|
|
5952a38ab9 | ||
|
|
e5fb19163e | ||
|
|
79f34a4fc7 | ||
|
|
3e90c08d55 | ||
|
|
697d8eca43 | ||
|
|
5cecf3104f | ||
|
|
a3a6caa6dd | ||
|
|
d3586c9039 | ||
|
|
71eafa287a | ||
|
|
828ef8580e | ||
|
|
a348c3a20a | ||
|
|
3c68c7f7cb | ||
|
|
820b2da16d | ||
|
|
6bdb2b94eb | ||
|
|
3c3db0b3a6 | ||
|
|
4748641c22 | ||
|
|
804787f2a3 | ||
|
|
b7c3b31e0c | ||
|
|
ab6781cd72 | ||
|
|
b070cfb227 | ||
|
|
5acb3a09e5 | ||
|
|
69c9f690cb | ||
|
|
d2a12ea834 | ||
|
|
16f24b4831 | ||
|
|
a3eeeab259 | ||
|
|
f69c254fbc | ||
|
|
f0f6bd4846 | ||
|
|
8816a18854 | ||
|
|
4e529e5c6c | ||
|
|
e521bf6f0b | ||
|
|
a91a0f86e0 | ||
|
|
f654003f95 | ||
|
|
4cc8a6fbf4 | ||
|
|
5167608404 | ||
|
|
9c93685431 | ||
|
|
a380087c9b | ||
|
|
3559e0d082 | ||
|
|
4323b0c2d1 | ||
|
|
3a800f7511 | ||
|
|
c203e5a38b | ||
|
|
3f5b3f06a2 | ||
|
|
d52635efbb | ||
|
|
4add4df08e | ||
|
|
df6d7e8df7 | ||
|
|
dc61640118 | ||
|
|
361108246e | ||
|
|
a5e25b9b28 | ||
|
|
ccff528765 | ||
|
|
24a1ef6721 | ||
|
|
b89919129e | ||
|
|
cbf3edefc7 | ||
|
|
909ae5f2fb | ||
|
|
0bbe5173dc | ||
|
|
84ad133077 | ||
|
|
43647a797f | ||
|
|
f469a9c127 | ||
|
|
212f7a374f | ||
|
|
1929d5c7d4 | ||
|
|
8ae54cd7cc | ||
|
|
bb13009e64 | ||
|
|
648dd862f5 | ||
|
|
8eccd3658e | ||
|
|
ed65b2a3ab | ||
|
|
0930121680 | ||
|
|
1068873f27 | ||
|
|
a1ece0be63 | ||
|
|
c550b4c8fc | ||
|
|
e95650637c | ||
|
|
0f3e0a1554 |
+13
-2
@@ -23,6 +23,8 @@ CALCOM_LICENSE_KEY=
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
UPSTASH_REDIS_REST_URL=
|
||||
UPSTASH_REDIS_REST_TOKEN=
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
@@ -57,6 +59,11 @@ SAML_CLIENT_SECRET_VERIFIER=
|
||||
# PGSSLMODE='no-verify'
|
||||
PGSSLMODE=
|
||||
|
||||
# Define which hostnames are expected for the app to work on
|
||||
ALLOWED_HOSTNAMES='"cal.com","cal.dev","cal-staging.com","cal.community","cal.local:3000","localhost:3000"'
|
||||
# Reserved orgs subdomains for our own usage
|
||||
RESERVED_SUBDOMAINS='"app","auth","docs","design","console","go","status","api","saml","www","matrix","developer","cal","my","team","support","security","blog","learn","admin"'
|
||||
|
||||
# - NEXTAUTH
|
||||
# @see: https://github.com/calendso/calendso/issues/263
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_url
|
||||
@@ -74,6 +81,10 @@ CALCOM_TELEMETRY_DISABLED=
|
||||
# ApiKey for cronjobs
|
||||
CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0'
|
||||
|
||||
# Whether to automatically keep app metadata in the database in sync with the metadata/config files. When disabled, the
|
||||
# sync runs in a reporting-only dry-run mode.
|
||||
CRON_ENABLE_APP_SYNC=false
|
||||
|
||||
# Application Key for symmetric encryption and decryption
|
||||
# must be 32 bytes for AES256 encryption algorithm
|
||||
# You can use: `openssl rand -base64 24` to generate one
|
||||
@@ -124,6 +135,7 @@ NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
@@ -170,7 +182,6 @@ CLOSECOM_API_KEY=
|
||||
# Sendgrid internal sync service
|
||||
SENDGRID_SYNC_API_KEY=
|
||||
|
||||
|
||||
# Change your Brand
|
||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||
NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS="help@cal.com"
|
||||
@@ -208,4 +219,4 @@ AUTH_BEARER_TOKEN_VERCEL=
|
||||
NEW_BOOKER_ENABLED_FOR_EMBED=0
|
||||
|
||||
#Enables New booker for All but Embed requests
|
||||
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions
|
||||
url: https://cal.com/slack
|
||||
about: Ask a general question about the project on our Slack workspace
|
||||
url: https://go.cal.com/discord
|
||||
about: Ask a general question about the project on our discord community
|
||||
|
||||
@@ -26,11 +26,13 @@ Fixes # (issue)
|
||||
- [ ] Test B
|
||||
|
||||
## Mandatory Tasks
|
||||
|
||||
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
|
||||
|
||||
## Checklist
|
||||
|
||||
<!-- Please remove all the irrelevant bullets to your PR -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
|
||||
@@ -9,7 +9,6 @@ runs:
|
||||
with:
|
||||
path: gh.env
|
||||
key: env-cache-${{ hashFiles('gh.env') }}
|
||||
restore-keys: env-cache-
|
||||
- name: Set Environment Variables
|
||||
uses: tw3lveparsecs/github-actions-setvars@latest
|
||||
with:
|
||||
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
schedule:
|
||||
# Runs every day (see https://crontab.guru)
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,3 +23,4 @@ jobs:
|
||||
days-before-pr-close: 7
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Cron - syncAppMeta
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “Every month at 1st (see https://crontab.guru)
|
||||
- cron: "0 0 1 * *"
|
||||
jobs:
|
||||
cron-syncAppMeta:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_API_KEY }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_API_KEY }}
|
||||
run: |
|
||||
curl ${{ secrets.APP_URL }}/api/cron/syncAppMeta \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
@@ -11,6 +11,7 @@ env:
|
||||
INPUT_ENV_GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
INPUT_ENV_GOOGLE_LOGIN_ENABLED: true
|
||||
# INPUT_ENV_CRON_API_KEY: xxx
|
||||
# INPUT_ENV_CRON_ENABLE_APP_SYNC: true|false
|
||||
INPUT_ENV_CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
INPUT_ENV_NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
INPUT_ENV_STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
@@ -31,6 +32,8 @@ env:
|
||||
# INPUT_ENV_MS_GRAPH_CLIENT_SECRET: xxx
|
||||
# INPUT_ENV_ZOOM_CLIENT_ID: xxx
|
||||
# INPUT_ENV_ZOOM_CLIENT_SECRET: xxx
|
||||
INPUT_ENV_SENDGRID_API_KEY: ${{ secrets.CI_SENDGRID_API_KEY }}
|
||||
INPUT_ENV_SENDGRID_EMAIL: ${{ secrets.CI_SENDGRID_EMAIL }}
|
||||
INPUT_ENV_TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
INPUT_ENV_TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
|
||||
|
||||
@@ -34,12 +34,15 @@ jobs:
|
||||
app-store:
|
||||
- 'apps/web/**'
|
||||
- 'packages/app-store/**'
|
||||
- 'playwright.config.ts'
|
||||
embed:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
embed-react:
|
||||
- 'apps/web/**'
|
||||
- 'packages/embeds/**'
|
||||
- 'playwright.config.ts'
|
||||
|
||||
env:
|
||||
name: Create env file
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
@@ -17,5 +17,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey there and thank you for opening this pull request! 👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions! 🙏
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: "Welcome new contributors"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: opened
|
||||
pull_request:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
welcome-message:
|
||||
name: Welcoming New Users
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: semantics
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |-
|
||||
Thank you for making your first Pull Request and taking the time to improve Cal.com ! ❤️🎉
|
||||
Feel free to join the conversation at [discord](https://go.cal.com/discord)
|
||||
issue-message: |
|
||||
Thank you for opening your first issue, one of our team members will review it as soon as it possible. ❤️🎉
|
||||
Vendored
+2
-1
@@ -8,6 +8,7 @@
|
||||
"ban.spellright", // Spell check for docs
|
||||
"stripe.vscode-stripe", // stripe VSCode extension
|
||||
"Prisma.prisma", // syntax|format|completion for prisma
|
||||
"rebornix.project-snippets" // Share useful snippets between collaborators
|
||||
"rebornix.project-snippets", // Share useful snippets between collaborators
|
||||
"inlang.vs-code-extension" // improved i18n DX
|
||||
]
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ To develop locally:
|
||||
|
||||
5. Set up your `.env` file:
|
||||
|
||||
- Duplicate `.env.example` to `.env`.
|
||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee and https://github.com/calcom/cal.com/tree/main/apps/api/ directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
|
||||
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
|
||||
+34
-16
@@ -1,39 +1,57 @@
|
||||
# Security
|
||||
|
||||
Contact: security@cal.com
|
||||
Contact: [security@cal.com](mailto:security@cal.com)
|
||||
|
||||
Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt)
|
||||
|
||||
At Cal.com, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
|
||||
At Cal.com, we consider the security of our systems a top priority. But no
|
||||
matter how much effort we put into system security, there can still be
|
||||
vulnerabilities present.
|
||||
|
||||
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.
|
||||
If you discover a vulnerability, we would like to know about it so we can take
|
||||
steps to address it as quickly as possible. We would like to ask you to help us
|
||||
better protect our clients and our systems.
|
||||
|
||||
## Out of scope vulnerabilities:
|
||||
## Out of scope vulnerabilities
|
||||
|
||||
- Clickjacking on pages with no sensitive actions.
|
||||
- Unauthenticated/logout/login CSRF.
|
||||
- Attacks requiring MITM or physical access to a user's device.
|
||||
- Any activity that could lead to the disruption of our service (DoS).
|
||||
- Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS.
|
||||
- Content spoofing and text injection issues without showing an attack
|
||||
vector/without being able to modify HTML/CSS.
|
||||
- Email spoofing
|
||||
- Missing DNSSEC, CAA, CSP headers
|
||||
- Lack of Secure or HTTP only flag on non-sensitive cookies
|
||||
- Deadlinks
|
||||
|
||||
## Please do the following:
|
||||
## Please do the following
|
||||
|
||||
- E-mail your findings to [security@cal.com](mailto:security@cal.com).
|
||||
- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you.
|
||||
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data,
|
||||
- Do not run automated scanners on our infrastructure or dashboard. If you wish
|
||||
to do this, contact us and we will set up a sandbox for you.
|
||||
- Do not take advantage of the vulnerability or problem you have discovered,
|
||||
for example by downloading more data than necessary to demonstrate the
|
||||
vulnerability or deleting or modifying other people's data,
|
||||
- Do not reveal the problem to others until it has been resolved,
|
||||
- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties,
|
||||
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation.
|
||||
- Do not use attacks on physical security, social engineering, distributed
|
||||
denial of service, spam or applications of third parties,
|
||||
- Do provide sufficient information to reproduce the problem, so we will be
|
||||
able to resolve it as quickly as possible. Usually, the IP address or the URL
|
||||
of the affected system and a description of the vulnerability will be
|
||||
sufficient, but complex vulnerabilities may require further explanation.
|
||||
|
||||
## What we promise:
|
||||
## What we promise
|
||||
|
||||
- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date,
|
||||
- If you have followed the instructions above, we will not take any legal action against you in regard to the report,
|
||||
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission,
|
||||
- We will respond to your report within 3 business days with our evaluation of
|
||||
the report and an expected resolution date,
|
||||
- If you have followed the instructions above, we will not take any legal
|
||||
action against you in regard to the report,
|
||||
- We will handle your report with strict confidentiality, and not pass on your
|
||||
personal details to third parties without your permission,
|
||||
- We will keep you informed of the progress towards resolving the problem,
|
||||
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and
|
||||
- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.
|
||||
- In the public information concerning the problem reported, we will give your
|
||||
name as the discoverer of the problem (unless you desire otherwise), and
|
||||
- We strive to resolve all problems as quickly as possible, and we would like
|
||||
to play an active role in the ultimate publication on the problem after it
|
||||
is resolved.
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
"description": "ApiKey for cronjobs",
|
||||
"value": ""
|
||||
},
|
||||
"CRON_ENABLE_APP_SYNC": {
|
||||
"description": "Whether to automatically keep app metadata in the database in sync with the metadata/config files. When disabled, the sync runs in a reporting-only dry-run mode.",
|
||||
"value": "false"
|
||||
},
|
||||
"SEND_FEEDBACK_EMAIL": {
|
||||
"description": "Send feedback email",
|
||||
"value": ""
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
The Cal.com Commercial License (the “Commercial License”)
|
||||
Copyright (c) 2020-present Cal.com, Inc
|
||||
|
||||
With regard to the Cal.com Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cal.com Subscription Terms available
|
||||
at https://cal.com/terms, or other agreements governing
|
||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
|
||||
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||
and to all such modifications and/or patches, and all such modifications and/or
|
||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Commercial Subscription for the correct number of hosts.
|
||||
Notwithstanding the foregoing, you may copy and modify the Software for development
|
||||
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
|
||||
its licensors (as applicable) retain all right, title and interest in and to all such
|
||||
modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
This Commercial License applies only to the part of this Software that is not distributed under
|
||||
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
|
||||
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
|
||||
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
|
||||
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
|
||||
be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cal.com Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
|
||||
</div>
|
||||
|
||||
# Cal.com Public API
|
||||
# Commercial Cal.com Public API
|
||||
|
||||
Welcome to the Public API ("/apps/api") of the Cal.com.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const dateNotInPast = function (date: Date) {
|
||||
export const verifyApiKey: NextMiddleware = async (req, res, next) => {
|
||||
const { prisma, isCustomPrisma, isAdmin } = req;
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
if (!hasValidLicense)
|
||||
if (!hasValidLicense && process.env.NODE_ENV !== "development")
|
||||
return res.status(401).json({ error: "Invalid or missing CALCOM_LICENSE_KEY environment variable" });
|
||||
// If the user is an admin and using a license key (from customPrisma), skip the apiKey check.
|
||||
if (isCustomPrisma && isAdmin) {
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"@sentry/nextjs": "^7.20.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "~13.2.1",
|
||||
"next": "^13.4.6",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
"next-validations": "^0.2.0",
|
||||
"typescript": "^4.9.4",
|
||||
|
||||
@@ -35,6 +35,9 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* status:
|
||||
* type: string
|
||||
* description: 'Acceptable values one of ["ACCEPTED", "PENDING", "CANCELLED", "REJECTED"]'
|
||||
* description:
|
||||
* type: string
|
||||
* description: 'Description of the meeting'
|
||||
* examples:
|
||||
* editBooking:
|
||||
* value:
|
||||
@@ -79,7 +82,15 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
||||
* "metadata": {},
|
||||
* "status": "CANCELLED"
|
||||
* "status": "CANCELLED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* 400:
|
||||
|
||||
@@ -26,13 +26,10 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* required:
|
||||
* - eventTypeId
|
||||
* - start
|
||||
* - name
|
||||
* - email
|
||||
* - responses
|
||||
* - timeZone
|
||||
* - language
|
||||
* - metadata
|
||||
* - customInputs
|
||||
* - location
|
||||
* properties:
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
@@ -45,29 +42,33 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 'End time of the Event'
|
||||
* name:
|
||||
* type: string
|
||||
* description: 'Name of the Attendee'
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 'Email ID of the Attendee'
|
||||
* responses:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - email
|
||||
* - location
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: 'Attendee full name'
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 'Attendee email address'
|
||||
* location:
|
||||
* type: string
|
||||
* description: 'Meeting location'
|
||||
* metadata:
|
||||
* type: object
|
||||
* properties: {}
|
||||
* description: 'Any metadata associated with the booking'
|
||||
* timeZone:
|
||||
* type: string
|
||||
* description: 'TimeZone of the Attendee'
|
||||
* language:
|
||||
* type: string
|
||||
* description: 'Language of the Attendee'
|
||||
* metadata:
|
||||
* type: object
|
||||
* properties: {}
|
||||
* description: 'Any metadata associated with the booking'
|
||||
* customInputs:
|
||||
* type: array
|
||||
* items: {}
|
||||
* location:
|
||||
* type: string
|
||||
* description: 'Meeting location'
|
||||
* title:
|
||||
* type: string
|
||||
* description: 'Booking event title'
|
||||
@@ -96,13 +97,14 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* "eventTypeId": 2323232,
|
||||
* "start": "2023-05-24T13:00:00.000Z",
|
||||
* "end": "2023-05-24T13:30:00.000Z",
|
||||
* "name": "Hello Hello",
|
||||
* "email": "hello@gmail.com",
|
||||
* "responses":{
|
||||
* "name": "Hello Hello",
|
||||
* "email": "hello@gmail.com",
|
||||
* "metadata": {},
|
||||
* "location": "Calcom HQ",
|
||||
* },
|
||||
* "timeZone": "Europe/London",
|
||||
* "language": "en",
|
||||
* "metadata": {},
|
||||
* "customInputs": [],
|
||||
* "location": "Calcom HQ",
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "description": null,
|
||||
* "status": "PENDING",
|
||||
@@ -117,53 +119,50 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* content:
|
||||
* application/json:
|
||||
* examples:
|
||||
* bookings:
|
||||
* booking created successfully example:
|
||||
* value:
|
||||
* {
|
||||
* "id": 11223344,
|
||||
* "uid": "5yUjmAYTDF6MXo98re8SkX",
|
||||
* "userId": 123,
|
||||
* "eventTypeId": 2323232,
|
||||
* "title": "Debugging between Syed Ali Shahbaz and Hello Hello",
|
||||
* "description": null,
|
||||
* "customInputs": {},
|
||||
* "responses": null,
|
||||
* "startTime": "2023-05-24T13:00:00.000Z",
|
||||
* "endTime": "2023-05-24T13:30:00.000Z",
|
||||
* "location": "Calcom HQ",
|
||||
* "createdAt": "2023-04-19T10:17:58.580Z",
|
||||
* "updatedAt": null,
|
||||
* "status": "PENDING",
|
||||
* "paid": false,
|
||||
* "destinationCalendarId": 2180,
|
||||
* "cancellationReason": null,
|
||||
* "rejectionReason": null,
|
||||
* "dynamicEventSlugRef": null,
|
||||
* "dynamicGroupSlugRef": null,
|
||||
* "rescheduled": null,
|
||||
* "fromReschedule": null,
|
||||
* "recurringEventId": null,
|
||||
* "smsReminderNumber": null,
|
||||
* "scheduledJobs": [],
|
||||
* "metadata": {},
|
||||
* "isRecorded": false,
|
||||
* "user": {
|
||||
* "email": "test@cal.com",
|
||||
* "name": "Syed Ali Shahbaz",
|
||||
* "timeZone": "Asia/Calcutta"
|
||||
* },
|
||||
* "attendees": [
|
||||
* {
|
||||
* "id": 12345,
|
||||
* "email": "hello@gmail.com",
|
||||
* "name": "Hello Hello",
|
||||
* "timeZone": "Europe/London",
|
||||
* "locale": "en",
|
||||
* "bookingId": 11223344
|
||||
* "booking": {
|
||||
* "id": 91,
|
||||
* "userId": 5,
|
||||
* "description": "",
|
||||
* "eventTypeId": 7,
|
||||
* "uid": "bFJeNb2uX8ANpT3JL5EfXw",
|
||||
* "title": "60min between Pro Example and John Doe",
|
||||
* "startTime": "2023-05-25T09:30:00.000Z",
|
||||
* "endTime": "2023-05-25T10:30:00.000Z",
|
||||
* "attendees": [
|
||||
* {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* }
|
||||
* ],
|
||||
* "user": {
|
||||
* "email": "pro@example.com",
|
||||
* "name": "Pro Example",
|
||||
* "timeZone": "Asia/Kolkata",
|
||||
* "locale": "en"
|
||||
* },
|
||||
* "payment": [
|
||||
* {
|
||||
* "id": 1,
|
||||
* "success": true,
|
||||
* "paymentOption": "ON_BOOKING"
|
||||
* }
|
||||
* ],
|
||||
* "metadata": {},
|
||||
* "status": "ACCEPTED",
|
||||
* "responses": {
|
||||
* "email": "john.doe@example.com",
|
||||
* "name": "John Doe",
|
||||
* "location": {
|
||||
* "optionValue": "",
|
||||
* "value": "inPerson"
|
||||
* }
|
||||
* }
|
||||
* ],
|
||||
* "payment": [],
|
||||
* "references": []
|
||||
* }
|
||||
* }
|
||||
* 400:
|
||||
* description: |
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
export default function getCalLink(eventType: {
|
||||
team?: { slug: string | null } | null;
|
||||
owner: { username: string | null } | null;
|
||||
owner?: { username: string | null } | null;
|
||||
users?: { username: string | null }[];
|
||||
slug: string;
|
||||
}) {
|
||||
|
||||
@@ -2,29 +2,36 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { publishHandler } from "@calcom/trpc/server/routers/viewer/teams/publish.handler";
|
||||
import { viewerTeamsRouter } from "@calcom/trpc/server/routers/viewer/teams/_router";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { getHTTPStatusCodeFromError } from "@trpc/server/http";
|
||||
|
||||
import { withMiddleware } from "~/lib/helpers/withMiddleware";
|
||||
import { schemaQueryTeamId } from "~/lib/validations/shared/queryTeamId";
|
||||
|
||||
import authMiddleware, { checkPermissions } from "./_auth-middleware";
|
||||
|
||||
const patchHandler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
await checkPermissions(req, { in: [MembershipRole.OWNER, MembershipRole.ADMIN] });
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res });
|
||||
const user = ctx.user;
|
||||
if (!user) {
|
||||
throw new Error("Internal Error.");
|
||||
async function sessionGetter() {
|
||||
return {
|
||||
user: {
|
||||
id: req.userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: req.isAdmin ? UserPermissionRole.ADMIN : UserPermissionRole.USER,
|
||||
},
|
||||
hasValidLicense: true /* To comply with TS signature */,
|
||||
expires: "" /* Not used in this context */,
|
||||
};
|
||||
}
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
try {
|
||||
const { teamId } = schemaQueryTeamId.parse(req.query);
|
||||
return await publishHandler({ input: { teamId }, ctx: { ...ctx, user } });
|
||||
const caller = viewerTeamsRouter.createCaller(ctx);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return await caller.publish(req.query as any /* Let tRPC handle this */);
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
|
||||
@@ -26,15 +26,15 @@ export function VariantsTable({
|
||||
<table>
|
||||
<RowTitles titles={titles} />
|
||||
{columns.map((column) => (
|
||||
<tr className="p-2 pr-6 pb-6" key={column.props.variant}>
|
||||
<tr className="p-2 pb-6 pr-6" key={column.props.variant}>
|
||||
<th
|
||||
className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]"
|
||||
className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]"
|
||||
key={column.props.variant}>
|
||||
{column.props.variant}
|
||||
</th>
|
||||
{React.Children.count(column.props.children) &&
|
||||
React.Children.map(column.props.children, (cell) => (
|
||||
<td className="p-2 pr-6 pb-6" style={{ minWidth: `${columnMinWidth}px` }}>
|
||||
<td className="p-2 pb-6 pr-6" style={{ minWidth: `${columnMinWidth}px` }}>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
@@ -70,9 +70,9 @@ export function VariantRow({ children }: RowProps) {
|
||||
export function RowTitles({ titles }: { titles: string[] }) {
|
||||
return (
|
||||
<tr>
|
||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" />
|
||||
<th className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]" />
|
||||
{titles.map((title) => (
|
||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" key={title}>
|
||||
<th className="p-2 pb-6 pr-6 text-left text-sm font-normal text-[#8F8F8F]" key={title}>
|
||||
{title}
|
||||
</th>
|
||||
))}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-id": "^1.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.0.0",
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"next": "^13.2.1",
|
||||
"next": "^13.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook-addon-rtl-direction": "^0.0.19"
|
||||
@@ -37,8 +37,8 @@
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"babel-loader": "^8.2.5",
|
||||
"fs": "^0.0.1-security",
|
||||
@@ -48,7 +48,7 @@
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.1.2"
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
--cal-bg-success: #E2FBE8;
|
||||
--cal-bg-attention: #FCEED8;
|
||||
--cal-bg-error: #F9E3E2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
|
||||
/* Borders */
|
||||
@@ -216,6 +217,7 @@
|
||||
--cal-border: #D1D5DB;
|
||||
--cal-border-subtle:#E5E7EB;
|
||||
--cal-border-muted:#F3F4F6;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #111827;
|
||||
@@ -235,6 +237,7 @@
|
||||
*/
|
||||
--cal-brand:#111827;
|
||||
--cal-brand-emphasis:#101010;
|
||||
--cal-brand-text: white;
|
||||
}
|
||||
.dark {
|
||||
/* background */
|
||||
@@ -250,6 +253,7 @@
|
||||
--cal-bg-success: #E2FBE8;
|
||||
--cal-bg-attention: #FCEED8;
|
||||
--cal-bg-error: #F9E3E2;
|
||||
--cal-bg-dark-error: #752522;
|
||||
|
||||
|
||||
/* Borders */
|
||||
@@ -257,6 +261,7 @@
|
||||
--cal-border: #444444;
|
||||
--cal-border-subtle: #2B2B2B;
|
||||
--cal-border-muted: #1C1C1C;
|
||||
--cal-border-error: #aa2e26;
|
||||
|
||||
/* Content/Text */
|
||||
--cal-text-emphasis: #F3F4F6;
|
||||
@@ -274,7 +279,8 @@
|
||||
/* Brand shenanigans
|
||||
-> These will be computed for the users theme at runtime.
|
||||
*/
|
||||
--cal-brand: #111827;
|
||||
--cal-brand-emphasis: #101010;
|
||||
--cal-brand: white;
|
||||
--cal-brand-emphasis: #e1e1e1;
|
||||
--cal-brand-text: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "^13.2.1",
|
||||
"next": "^13.4.6",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import { components } from "react-select";
|
||||
|
||||
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
|
||||
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { APP_NAME, EMBED_LIB_URL, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogFooter,
|
||||
DialogContent,
|
||||
HorizontalTabs,
|
||||
Label,
|
||||
@@ -38,13 +39,21 @@ const enum Theme {
|
||||
dark = "dark",
|
||||
}
|
||||
|
||||
const EMBED_CAL_ORIGIN = WEBAPP_URL;
|
||||
const EMBED_CAL_JS_URL = `${WEBAPP_URL}/embed/embed.js`;
|
||||
|
||||
type PreviewState = {
|
||||
inline: {
|
||||
width: string;
|
||||
height: string;
|
||||
};
|
||||
theme: Theme;
|
||||
floatingPopup: Record<string, string | boolean | undefined>;
|
||||
floatingPopup: {
|
||||
config?: {
|
||||
layout: BookerLayouts;
|
||||
};
|
||||
[key: string]: string | boolean | undefined | Record<string, string>;
|
||||
};
|
||||
elementClick: Record<string, string>;
|
||||
palette: {
|
||||
brandColor: string;
|
||||
@@ -175,14 +184,24 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
export default function MyApp() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
return <Cal calLink="${calLink}" style={{width:"${width}",height:"${height}",overflow:"scroll"}} />;
|
||||
return <Cal
|
||||
calLink="${calLink}"
|
||||
style={{width:"${width}",height:"${height}",overflow:"scroll"}}
|
||||
${previewState.layout ? "config={{layout: '" + previewState.layout + "'}}" : ""}${
|
||||
IS_SELF_HOSTED
|
||||
? `
|
||||
calOrigin="${EMBED_CAL_ORIGIN}"
|
||||
calJsUrl="${EMBED_CAL_JS_URL}"`
|
||||
: ""
|
||||
}
|
||||
/>;
|
||||
};`;
|
||||
},
|
||||
"floating-popup": ({
|
||||
@@ -193,38 +212,60 @@ function MyComponent() {
|
||||
uiInstructionCode: string;
|
||||
}) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
import { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
export default function App() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""});
|
||||
cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
};`;
|
||||
},
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
"element-click": ({
|
||||
calLink,
|
||||
uiInstructionCode,
|
||||
previewState,
|
||||
}: {
|
||||
calLink: string;
|
||||
uiInstructionCode: string;
|
||||
previewState: PreviewState;
|
||||
}) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
import { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
export default function App() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${EMBED_CAL_JS_URL}"` : ""});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
return <button data-cal-link="${calLink}" />;
|
||||
return <button
|
||||
data-cal-link="${calLink}"${IS_SELF_HOSTED ? `\ndata-cal-origin="${EMBED_CAL_ORIGIN}"` : ""}
|
||||
${`data-cal-config='${JSON.stringify({
|
||||
layout: previewState.layout,
|
||||
})}'`}
|
||||
>Click me</button>;
|
||||
};`;
|
||||
},
|
||||
},
|
||||
HTML: {
|
||||
inline: ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
inline: ({
|
||||
calLink,
|
||||
uiInstructionCode,
|
||||
previewState,
|
||||
}: {
|
||||
calLink: string;
|
||||
uiInstructionCode: string;
|
||||
previewState: PreviewState;
|
||||
}) => {
|
||||
return code`Cal("inline", {
|
||||
elementOrSelector:"#my-cal-inline",
|
||||
calLink: "${calLink}"
|
||||
calLink: "${calLink}",
|
||||
layout: "${previewState.layout}"
|
||||
});
|
||||
|
||||
${uiInstructionCode}`;
|
||||
@@ -240,8 +281,22 @@ ${uiInstructionCode}`;
|
||||
return code`Cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}`;
|
||||
},
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
return code`// Important: Make sure to add \`data-cal-link="${calLink}"\` attribute to the element you want to open Cal on click
|
||||
"element-click": ({
|
||||
calLink,
|
||||
uiInstructionCode,
|
||||
previewState,
|
||||
}: {
|
||||
calLink: string;
|
||||
uiInstructionCode: string;
|
||||
previewState: PreviewState;
|
||||
}) => {
|
||||
return code`
|
||||
// Important: Please add following attributes to the element you want to open Cal on click
|
||||
// \`data-cal-link="${calLink}"\`
|
||||
// \`data-cal-config='${JSON.stringify({
|
||||
layout: previewState.layout,
|
||||
})}'\`
|
||||
|
||||
${uiInstructionCode}`;
|
||||
},
|
||||
},
|
||||
@@ -298,6 +353,7 @@ const getEmbedTypeSpecificString = ({
|
||||
} else if (embedType === "floating-popup") {
|
||||
const floatingButtonArg = {
|
||||
calLink,
|
||||
...(IS_SELF_HOSTED ? { calOrigin: EMBED_CAL_ORIGIN } : null),
|
||||
...previewState.floatingPopup,
|
||||
};
|
||||
return frameworkCodes[embedType]({
|
||||
@@ -710,13 +766,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
height: "100%",
|
||||
},
|
||||
theme: Theme.auto,
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
floatingPopup: {},
|
||||
elementClick: {},
|
||||
hideEventTypeDetails: false,
|
||||
palette: {
|
||||
brandColor: "#000000",
|
||||
},
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
@@ -778,8 +834,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
name: "ui",
|
||||
arg: {
|
||||
theme: previewState.theme,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
layout: previewState.layout,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
styles: {
|
||||
branding: {
|
||||
...previewState.palette,
|
||||
@@ -1000,7 +1056,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div className="whitespace-nowrap">Text color</div>
|
||||
<div className="mt-2 mb-6 w-40 xl:mt-0 xl:w-full">
|
||||
<div className="mb-6 mt-2 w-40 xl:mt-0 xl:w-full">
|
||||
<ColorPicker
|
||||
className="w-[130px]"
|
||||
popoverAlign="start"
|
||||
@@ -1102,8 +1158,15 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
return;
|
||||
}
|
||||
setPreviewState((previewState) => {
|
||||
const config = {
|
||||
...(previewState.floatingPopup.config ?? {}),
|
||||
layout: option.value,
|
||||
};
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
config,
|
||||
},
|
||||
layout: option.value,
|
||||
};
|
||||
});
|
||||
@@ -1144,8 +1207,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={router.query.embedTabName == "embed-preview" ? "block" : "hidden"} />
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<div className={router.query.embedTabName == "embed-preview" ? "mt-2 block" : "hidden"} />
|
||||
<DialogFooter
|
||||
className="mt-10 flex flex-row-reverse gap-x-2"
|
||||
showDivider
|
||||
customDividerClassNames="w-2/3">
|
||||
<DialogClose />
|
||||
{tab.type === "code" ? (
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -1160,8 +1227,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
{t("copy_code")}
|
||||
</Button>
|
||||
) : null}
|
||||
<DialogClose />
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -71,7 +71,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
isCurrent
|
||||
? "text-emphasis border-gray-900"
|
||||
: "hover:border-default hover:text-default text-subtle border-transparent",
|
||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||
"group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
|
||||
@@ -85,7 +85,7 @@ const Component = ({
|
||||
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">
|
||||
{hasDescriptionItems && (
|
||||
<div className="align-center bg-subtle mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||
<div className="align-center bg-subtle -ml-4 -mr-4 mb-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap p-4 md:-ml-8 md:-mr-8 md:mb-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||
{descriptionItems ? (
|
||||
descriptionItems.map((descriptionItem, index) =>
|
||||
typeof descriptionItem === "object" ? (
|
||||
@@ -260,7 +260,7 @@ const Component = ({
|
||||
)}
|
||||
</span>
|
||||
|
||||
<h4 className="text-emphasis mt-8 mb-2 font-semibold ">{t("contact")}</h4>
|
||||
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
|
||||
<ul className="prose-sm -ml-1 -mr-1 leading-5">
|
||||
{docs && (
|
||||
<li>
|
||||
@@ -269,7 +269,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
href={docs}>
|
||||
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
||||
<BookOpen className="text-subtle -mt-1 mr-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -281,7 +281,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={website}>
|
||||
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||
<ExternalLink className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -293,7 +293,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -306,7 +306,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={tos}>
|
||||
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||
<File className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -318,7 +318,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||
<Shield className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -28,8 +28,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
{emptyStore ? (
|
||||
<EmptyScreen
|
||||
Icon={AlertCircle}
|
||||
headline={t("no_apps")}
|
||||
description={isAdmin ? "You can enable apps in the settings" : ""}
|
||||
headline={isAdmin ? t("no_apps") : t("no_apps_configured")}
|
||||
description={isAdmin ? t("enable_in_settings") : t("please_contact_admin")}
|
||||
buttonText={isAdmin ? t("apps_settings") : ""}
|
||||
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
@@ -23,20 +21,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
return null;
|
||||
}
|
||||
|
||||
const translateAbleKeys = [
|
||||
"in_person_attendee_address",
|
||||
"in_person",
|
||||
"attendee_phone_number",
|
||||
"link_meeting",
|
||||
"organizer_phone_number",
|
||||
];
|
||||
|
||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||
const translatedLocation = location.type.startsWith("integrations:")
|
||||
? eventLocationType.label
|
||||
: translateAbleKeys.includes(locationKey)
|
||||
? t(locationKey)
|
||||
: locationKey;
|
||||
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
|
||||
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
|
||||
@@ -83,7 +83,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
size="sm"
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">
|
||||
<h2 className="text-default mb-2 mt-1 break-words text-sm font-medium ">
|
||||
{eventType.team?.parent?.name} {profile.name}
|
||||
</h2>
|
||||
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
|
||||
|
||||
@@ -19,7 +19,7 @@ const BookingDescriptionPayment = (props: {
|
||||
|
||||
return (
|
||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>{props.t("no_show_fee_amount", params)}</>
|
||||
) : (
|
||||
|
||||
@@ -374,7 +374,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</td>
|
||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
||||
{/* Time and Badges for mobile */}
|
||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||
<div className="flex w-full items-center justify-between sm:hidden">
|
||||
<div className="text-emphasis text-sm leading-6">{startTime}</div>
|
||||
<div className="text-subtle pr-2 text-sm">
|
||||
@@ -423,7 +423,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<span> </span>
|
||||
|
||||
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||
<Badge className="ms-2 me-2 hidden sm:inline-flex" variant="orange">
|
||||
<Badge className="me-2 ms-2 hidden sm:inline-flex" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function CancelBooking(props: Props) {
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-4 w-full "
|
||||
className="mb-4 mt-2 w-full "
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||
@@ -93,7 +93,7 @@ export default function CancelBooking(props: Props) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
|
||||
@@ -25,7 +25,7 @@ function SkeletonItem() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:ml-5 sm:mt-0 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText className="h-6 w-16" />
|
||||
<SkeletonText className="h-6 w-32" />
|
||||
|
||||
@@ -191,9 +191,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="flex items-start text-sm font-medium">
|
||||
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
<RefreshCcw className="float-left ml-[2px] mt-[7px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
<div>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
<p className="-ml-2 mb-1 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
|
||||
@@ -204,7 +204,13 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
isFullWidth={false}
|
||||
className="me-2 inline w-16"
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
const count =
|
||||
eventType?.recurringEvent?.count &&
|
||||
parseInt(event?.target.value) > eventType?.recurringEvent?.count
|
||||
? eventType.recurringEvent.count
|
||||
: parseInt(event?.target.value);
|
||||
|
||||
setRecurringEventCount(count);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -218,7 +224,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
)}
|
||||
{paymentAppData.price > 0 && (
|
||||
<p className="-ml-2 px-2 text-sm font-medium">
|
||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>
|
||||
{t("no_show_fee_amount", {
|
||||
|
||||
@@ -534,7 +534,7 @@ const BookingPage = ({
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
{showEventTypeDetails && (
|
||||
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<div className="sm:border-subtle text-default flex flex-col px-6 pb-0 pt-6 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
@@ -561,7 +561,7 @@ const BookingPage = ({
|
||||
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
|
||||
return <p key={key}>{timeFormatted}</p>;
|
||||
})}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
{!rescheduleUid && eventType?.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.slice(5).map((timeFormatted, key) => (
|
||||
@@ -577,11 +577,11 @@ const BookingPage = ({
|
||||
</div>
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
|
||||
<p className="mb-2 mt-8 text-sm " data-testid="former_time_p">
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="line-through ">
|
||||
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<Calendar className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{isClientTimezoneAvailable &&
|
||||
typeof booking.startTime === "string" &&
|
||||
parseDateTimeWithTimeZone(booking.startTime, i18n.language, timezone, {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getHumanReadableLocationValue,
|
||||
getMessageForOrganizer,
|
||||
LocationType,
|
||||
OrganizerDefaultConferencingAppType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
@@ -172,8 +173,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
}
|
||||
);
|
||||
|
||||
console.log(defaultLocation);
|
||||
|
||||
const LocationOptions = (() => {
|
||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||
if (!eventLocationType.variable) {
|
||||
@@ -259,7 +258,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
|
||||
{booking && (
|
||||
<>
|
||||
<p className="text-emphasis mt-6 mb-2 ml-1 text-sm font-bold">{t("current_location")}:</p>
|
||||
<p className="text-emphasis mb-2 ml-1 mt-6 text-sm font-bold">{t("current_location")}:</p>
|
||||
<p className="text-emphasis mb-2 ml-1 text-sm">
|
||||
{getHumanReadableLocationValue(booking.location, t)}
|
||||
</p>
|
||||
@@ -311,8 +310,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
query={locationsQuery}
|
||||
success={({ data }) => {
|
||||
if (!data.length) return null;
|
||||
const locationOptions = [...data].filter((option) => {
|
||||
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||
const locationOptions = [...data].map((option) => {
|
||||
if (isTeamEvent) {
|
||||
// Let host's Default conferencing App option show for Team Event
|
||||
return option;
|
||||
}
|
||||
return {
|
||||
...option,
|
||||
options: option.options.filter((o) => o.value !== OrganizerDefaultConferencingAppType),
|
||||
};
|
||||
});
|
||||
if (booking) {
|
||||
locationOptions.map((location) =>
|
||||
|
||||
@@ -49,7 +49,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
<p className="text-subtle text-sm">{t("reschedule_modal_description")}</p>
|
||||
<p className="text-emphasis mt-6 mb-2 text-sm font-bold">
|
||||
<p className="text-emphasis mb-2 mt-6 text-sm font-bold">
|
||||
{t("reason_for_reschedule_request")}
|
||||
<span className="text-subtle font-normal"> (Optional)</span>
|
||||
</p>
|
||||
|
||||
@@ -35,7 +35,7 @@ const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (
|
||||
{debugMap.map(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
return (
|
||||
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:py-5 sm:px-6">
|
||||
<div key={key} className="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<dt className="text-emphasis text-sm font-bold">{key}</dt>
|
||||
<dd className="text-emphasis mt-1 text-sm sm:col-span-2 sm:mt-0">{value}</dd>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ const CustomEventTypeModalForm: FC<CustomEventTypeModalFormProps> = (props) => {
|
||||
backgroundImage: "url(/calendar-preview.svg)",
|
||||
}}>
|
||||
<div className="m-auto flex items-center justify-center self-stretch">
|
||||
<div className="bg-subtle mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
||||
<div className="bg-subtle ml-11 mt-3 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black text-[12px] leading-3">
|
||||
<p className="text-emphasis overflow-hidden text-ellipsis p-1.5 font-medium">{previewText}</p>
|
||||
<p className="text-default ml-1.5 text-[10px] font-normal">8 - 10 AM</p>
|
||||
</div>
|
||||
|
||||
@@ -38,6 +38,7 @@ const generateHashedLink = (id: number) => {
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -49,6 +50,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name + " input";
|
||||
@@ -153,7 +157,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
color="minimal"
|
||||
size="sm"
|
||||
aria-label="edit custom name"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -162,7 +166,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</div>
|
||||
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||
<div>
|
||||
<BookerLayoutSelector fallbackToUserSettings />
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} />
|
||||
</div>
|
||||
<hr className="border-subtle" />
|
||||
<FormBuilder
|
||||
@@ -216,7 +220,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
onChange(e ? value : "");
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||
<div className="lg:-ml-2 lg:-mb-2">
|
||||
<div className="lg:-mb-2 lg:-ml-2">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label={t("redirect_success_booking")}
|
||||
@@ -268,7 +272,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
|
||||
@@ -171,7 +171,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4 lg:space-y-8">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeBufferTime">
|
||||
{t("before_event")}
|
||||
@@ -245,7 +245,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="minimumBookingNotice">
|
||||
{t("minimum_booking_notice")}
|
||||
@@ -416,7 +416,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
</div>
|
||||
)}
|
||||
{period.type === "RANGE" && (
|
||||
<div className="ms-2 me-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
control={formMethods.control}
|
||||
@@ -437,7 +437,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{period.suffix ? <span className="ms-2 me-2"> {period.suffix}</span> : null}
|
||||
{period.suffix ? <span className="me-2 ms-2"> {period.suffix}</span> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -117,8 +117,16 @@ export const EventSetupTab = (
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
||||
|
||||
const locationOptions = props.locationOptions.filter((option) => {
|
||||
return !team ? option.label !== "Conferencing" : true;
|
||||
const locationOptions = props.locationOptions.map((locationOption) => {
|
||||
const options = locationOption.options.filter((option) => {
|
||||
// Skip "Organizer's Default App" for non-team members
|
||||
return !team ? option.label !== t("organizer_default_conferencing_app") : true;
|
||||
});
|
||||
|
||||
return {
|
||||
...locationOption,
|
||||
options,
|
||||
};
|
||||
});
|
||||
|
||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||
@@ -284,7 +292,7 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<li
|
||||
key={`${location.type}${index}`}
|
||||
className="border-default text-default mb-2 h-9 rounded-md border py-1.5 px-2 hover:cursor-pointer">
|
||||
className="border-default text-default mb-2 h-9 rounded-md border px-2 py-1.5 hover:cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<img
|
||||
@@ -298,7 +306,7 @@ export const EventSetupTab = (
|
||||
)}
|
||||
alt={`${eventLocationType.label} logo`}
|
||||
/>
|
||||
<span className="line-clamp-1 ms-1 text-sm">{eventLabel}</span>
|
||||
<span className="ms-1 line-clamp-1 text-sm">{eventLabel}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
@@ -332,7 +340,7 @@ export const EventSetupTab = (
|
||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||
) && (
|
||||
<div className="text-default flex text-sm">
|
||||
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||
<Check className="mr-1.5 mt-0.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.
|
||||
|
||||
@@ -369,7 +369,7 @@ function EventTypeSingleLayout({
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between py-2 px-4">
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { ConfirmationDialogContent, Dialog } from "@calcom/ui";
|
||||
|
||||
interface ManagedEventDialogProps {
|
||||
slugExistsChildrenDialogOpen: ChildrenEventType[];
|
||||
slug: string;
|
||||
onOpenChange: () => void;
|
||||
isLoading: boolean;
|
||||
onConfirm: (e: { preventDefault: () => void }) => void;
|
||||
}
|
||||
|
||||
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
|
||||
const { t } = useLocale();
|
||||
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
|
||||
|
||||
return (
|
||||
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={isLoading}
|
||||
variety="warning"
|
||||
title={t("managed_event_dialog_title", {
|
||||
slug,
|
||||
count: slugExistsChildrenDialogOpen.length,
|
||||
})}
|
||||
confirmBtnText={t("managed_event_dialog_confirm_button", {
|
||||
count: slugExistsChildrenDialogOpen.length,
|
||||
})}
|
||||
cancelBtnText={t("go_back")}
|
||||
onConfirm={onConfirm}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey="managed_event_dialog_information"
|
||||
values={{
|
||||
names: `${slugExistsChildrenDialogOpen
|
||||
.map((ch) => ch.owner.name)
|
||||
.slice(0, -1)
|
||||
.join(", ")} ${
|
||||
slugExistsChildrenDialogOpen.length > 1 ? t("and") : ""
|
||||
} ${slugExistsChildrenDialogOpen.map((ch) => ch.owner.name).slice(-1)}`,
|
||||
slug,
|
||||
}}
|
||||
count={slugExistsChildrenDialogOpen.length}
|
||||
/>
|
||||
</p>{" "}
|
||||
<p className="mt-5">{t("managed_event_dialog_clarification")}</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
||||
<div className="">
|
||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||
<Clock className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||
<User className="text-subtle mr-1.5 mt-0.5 inline h-4 w-4" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, Form, PasswordField } from "@calcom/ui";
|
||||
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
|
||||
@@ -75,14 +75,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
||||
{...form.register("password")}
|
||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||
/>
|
||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||
|
||||
<TwoFactor center={false} />
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button type="submit" className="ms-2 me-2" disabled={isDisabling}>
|
||||
<Button type="submit" className="me-2 ms-2" disabled={isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
|
||||
@@ -19,7 +19,7 @@ const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonati
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
|
||||
<div>
|
||||
<div className="flex flex-row items-center">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||
|
||||
@@ -178,19 +178,19 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ms-2 me-2"
|
||||
className="me-2 ms-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<Button type="submit" className="ms-2 me-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
<Button type="submit" className="me-2 ms-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<Button type="submit" className="ms-2 me-2" disabled={isSubmitting}>
|
||||
<Button type="submit" className="me-2 ms-2" disabled={isSubmitting}>
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
|
||||
@@ -14,7 +14,7 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-between pt-9 pl-2 sm:flex-row">
|
||||
<div className="flex flex-col justify-between pl-2 pt-9 sm:flex-row">
|
||||
<div>
|
||||
<div className="flex flex-row items-center">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">{t("2fa")}</h2>
|
||||
|
||||
@@ -6,7 +6,7 @@ const TwoFactorModalHeader = ({ title, description }: { title: string; descripti
|
||||
<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">
|
||||
<Shield className="text-inverted h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField } from "@calcom/ui";
|
||||
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
|
||||
@@ -74,7 +74,7 @@ const DisableTwoFactorAuthModal = ({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent title={t("disable_2fa")} description={t("disable_2fa_recommendation")} type="creation">
|
||||
<Form form={form} handleSubmit={handleDisable}>
|
||||
<div className="mb-4">
|
||||
<div className="mb-8">
|
||||
<PasswordField
|
||||
labelProps={{
|
||||
className: "block text-sm font-medium text-default",
|
||||
@@ -82,21 +82,19 @@ const DisableTwoFactorAuthModal = ({
|
||||
{...form.register("password")}
|
||||
className="border-default mt-1 block w-full rounded-md border px-3 py-2 text-sm focus:border-black focus:outline-none focus:ring-black"
|
||||
/>
|
||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||
|
||||
<TwoFactor center={false} />
|
||||
|
||||
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button type="submit" className="ms-2 me-2" data-testid="disable-2fa" disabled={isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<DialogFooter showDivider className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="submit" className="me-2 ms-2" data-testid="disable-2fa" disabled={isDisabling}>
|
||||
{t("disable")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, TextField } from "@calcom/ui";
|
||||
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
|
||||
@@ -150,20 +150,20 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className="-mt-3 flex justify-center">
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={dataUri} alt="" />
|
||||
}
|
||||
</div>
|
||||
<p data-testid="two-factor-secret" className="text-center font-mono text-xs">
|
||||
<p data-testid="two-factor-secret" className="mb-4 text-center font-mono text-xs">
|
||||
{secret}
|
||||
</p>
|
||||
</>
|
||||
</WithStep>
|
||||
<Form handleSubmit={handleEnable} form={form}>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<div className="mb-4">
|
||||
<div className="-mt-4 pb-2">
|
||||
<TwoFactor center />
|
||||
|
||||
{errorMessage && (
|
||||
@@ -173,11 +173,14 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
)}
|
||||
</div>
|
||||
</WithStep>
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<DialogFooter className="mt-8 sm:flex sm:flex-row-reverse" showDivider>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<Button
|
||||
type="submit"
|
||||
className="ms-2 me-2"
|
||||
className="me-2 ms-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
{t("continue")}
|
||||
@@ -187,20 +190,17 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="goto-otp-screen"
|
||||
className="ms-2 me-2"
|
||||
className="me-2 ms-2"
|
||||
onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
<Button type="submit" className="ms-2 me-2" data-testid="enable-2fa" disabled={isSubmitting}>
|
||||
<Button type="submit" className="me-2 ms-2" data-testid="enable-2fa" disabled={isSubmitting}>
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
|
||||
<Label htmlFor="username" className={classNames(longWebsiteUrl && "mb-0")}>
|
||||
<span className="block">{t("username")}</span>
|
||||
{longWebsiteUrl && (
|
||||
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 py-1 px-3">
|
||||
<small className="items-centerpx-3 bg-subtle border-default text-subtle mt-2 inline-flex rounded-t-md border border-b-0 px-3 py-1">
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}
|
||||
</small>
|
||||
)}
|
||||
|
||||
@@ -27,7 +27,7 @@ const StepDone = (props: {
|
||||
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
||||
</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>
|
||||
<h2 className="mb-1 mt-6 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -30,9 +30,9 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
alt={member.name || ""}
|
||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||
/>
|
||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
||||
<p className="text-default font-medium">{member.name}</p>
|
||||
<div className="line-clamp-3 text-subtle overflow-ellipsis text-sm font-normal">
|
||||
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
|
||||
{!isBioEmpty ? (
|
||||
<>
|
||||
<div
|
||||
|
||||
@@ -23,7 +23,7 @@ const EditableHeading = function EditableHeading({
|
||||
<label className="min-w-8 relative inline-block">
|
||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||
{!isEditing && isReady && (
|
||||
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
|
||||
<Edit2 className=" text-subtle group-hover:text-subtle -mt-px ml-1 inline h-3 w-3" />
|
||||
)}
|
||||
<input
|
||||
{...passThroughProps}
|
||||
@@ -31,7 +31,7 @@ const EditableHeading = function EditableHeading({
|
||||
value={value}
|
||||
required
|
||||
className={classNames(
|
||||
"text-emphasis hover:text-default focus:text-emphasis absolute top-0 left-0 w-full cursor-pointer truncate border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
||||
"text-emphasis hover:text-default focus:text-emphasis absolute left-0 top-0 w-full cursor-pointer truncate border-none bg-transparent p-0 align-top text-xl focus:outline-none focus:ring-0"
|
||||
)}
|
||||
onFocus={(e) => {
|
||||
setIsEditing(true);
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function InfoBadge({ content }: { content: string }) {
|
||||
<>
|
||||
<Tooltip side="top" content={content}>
|
||||
<span title={content}>
|
||||
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
|
||||
<Info className="text-subtle relative left-1 right-1 top-px mt-px h-4 w-4" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function ModalContainer(
|
||||
}>
|
||||
) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
|
||||
<Dialog open={props.isOpen} onOpenChange={props.onExit}>
|
||||
<DialogContent>
|
||||
<div
|
||||
|
||||
@@ -224,7 +224,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
}}
|
||||
data-testid="username-input"
|
||||
/>
|
||||
<div className="absolute top-0 right-2 flex flex-row">
|
||||
<div className="absolute right-2 top-0 flex flex-row">
|
||||
<span
|
||||
className={classNames(
|
||||
"mx-2 py-2",
|
||||
|
||||
@@ -80,7 +80,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
|
||||
const ActionButtons = () => {
|
||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||
<div className="ms-2 me-2 flex flex-row space-x-2">
|
||||
<div className="me-2 ms-2 flex flex-row space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpenDialogSaveUsername(true)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
@@ -24,9 +24,9 @@ interface UsernameAvailabilityFieldProps {
|
||||
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
|
||||
return organization
|
||||
? organization.slug
|
||||
? `${organization.slug}.${subdomainSuffix()}`
|
||||
? getOrgFullDomain(organization.slug, { protocol: false })
|
||||
: organization.metadata && organization.metadata.requestedSlug
|
||||
? `${organization.metadata.requestedSlug}.${subdomainSuffix()}`
|
||||
? getOrgFullDomain(organization.metadata.requestedSlug, { protocol: false })
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")
|
||||
: process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const getDefaultSubdomain = (url) => {
|
||||
if (!url.startsWith("http:") && !url.startsWith("https:")) {
|
||||
// Make it a valid URL. Mabe we can simply return null and opt-out from orgs support till the use a URL scheme.
|
||||
url = `https://${url}`;
|
||||
}
|
||||
const _url = new URL(url);
|
||||
const regex = new RegExp(/^([a-z]+\:\/{2})?((?<subdomain>[\w-.]+)\.[\w-]+\.\w+)$/);
|
||||
//console.log(_url.hostname, _url.hostname.match(regex));
|
||||
return _url.hostname.match(regex)?.groups?.subdomain || null;
|
||||
};
|
||||
exports.getSubdomainRegExp = (url) => {
|
||||
const defaultSubdomain = getDefaultSubdomain(url);
|
||||
const subdomain = defaultSubdomain ? `(?!${defaultSubdomain})[^.]+` : "[^.]+";
|
||||
return subdomain;
|
||||
};
|
||||
@@ -5,7 +5,8 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { Maybe } from "@calcom/trpc/server";
|
||||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { i18n } = require("@calcom/config/next-i18next.config");
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
let preferredLocale: string | null | undefined;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { collectEvents } from "next-collect/server";
|
||||
import type { NextMiddleware } from "next/server";
|
||||
import { NextResponse, userAgent } from "next/server";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { isIpInBanlist } from "@calcom/lib/getIP";
|
||||
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
|
||||
@@ -11,20 +10,11 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
||||
const middleware: NextMiddleware = async (req) => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.get("host") ?? "");
|
||||
/**
|
||||
* We are using env variable to toggle new-booker because using flags would be an unnecessary delay for booking pages
|
||||
* Also, we can't easily identify the booker page requests here(to just fetch the flags for those requests)
|
||||
*/
|
||||
|
||||
// Make sure we are in the presence of an organization
|
||||
if (isValidOrgDomain && url.pathname === "/") {
|
||||
// In the presence of an organization, cover its profile page at "/"
|
||||
// rewrites for org profile page using team profile page
|
||||
url.pathname = `/org/${currentOrgDomain}`;
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
|
||||
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
|
||||
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback
|
||||
req.nextUrl.pathname = "/api/nope";
|
||||
@@ -83,26 +73,6 @@ const middleware: NextMiddleware = async (req) => {
|
||||
requestHeaders.set("x-csp-enforce", "true");
|
||||
}
|
||||
|
||||
if (isValidOrgDomain) {
|
||||
// Match /:slug to determine if it corresponds to org subteam slug or org user slug
|
||||
const slugs = /^\/([^/]+)(\/[^/]+)?$/.exec(url.pathname);
|
||||
// In the presence of an organization, if not team profile, a user or team is being accessed
|
||||
if (slugs) {
|
||||
const [_, teamName, eventType] = slugs;
|
||||
// Fetch the corresponding subteams for the entered organization
|
||||
const getSubteams = await fetch(`${WEBAPP_URL}/api/organizations/${currentOrgDomain}/subteams`);
|
||||
if (getSubteams.ok) {
|
||||
const data = await getSubteams.json();
|
||||
// Treat entered slug as a team if found in the subteams fetched
|
||||
if (data.slugs.includes(teamName)) {
|
||||
// Rewriting towards /team/:slug to bring up the team profile within the org
|
||||
url.pathname = `/team/${teamName}${eventType ?? ""}`;
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders,
|
||||
|
||||
+120
-28
@@ -1,10 +1,11 @@
|
||||
require("dotenv").config({ path: "../../.env" });
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const os = require("os");
|
||||
const glob = require("glob");
|
||||
const englishTranslation = require("./public/static/locales/en/common.json");
|
||||
const { withAxiom } = require("next-axiom");
|
||||
const { i18n } = require("./next-i18next.config");
|
||||
const { pages } = require("./pages");
|
||||
const { getSubdomainRegExp } = require("./getSubdomainRegExp");
|
||||
|
||||
if (!process.env.NEXTAUTH_SECRET) throw new Error("Please set NEXTAUTH_SECRET");
|
||||
if (!process.env.CALENDSO_ENCRYPTION_KEY) throw new Error("Please set CALENDSO_ENCRYPTION_KEY");
|
||||
@@ -70,7 +71,6 @@ const informAboutDuplicateTranslations = () => {
|
||||
};
|
||||
|
||||
informAboutDuplicateTranslations();
|
||||
|
||||
const plugins = [];
|
||||
if (process.env.ANALYZE === "true") {
|
||||
// only load dependency if env `ANALYZE` was set
|
||||
@@ -82,16 +82,52 @@ if (process.env.ANALYZE === "true") {
|
||||
|
||||
plugins.push(withAxiom);
|
||||
|
||||
/** Needed to rewrite public booking page, gets all static pages but [user] */
|
||||
const pages = glob
|
||||
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname })
|
||||
.map((filename) =>
|
||||
filename
|
||||
.substr(6)
|
||||
.replace(/(\.tsx|\.js|\.ts)/, "")
|
||||
.replace(/\/.*/, "")
|
||||
)
|
||||
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]"));
|
||||
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
||||
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
||||
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
||||
// ?!book ensures it doesn't match /free/book page which doesn't have a corresponding new-booker page.
|
||||
// [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
|
||||
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
|
||||
|
||||
// Important Note: When modifying these RegExps update apps/web/test/lib/next-config.test.ts as well
|
||||
const userTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type((?!book$)[^/]+)`;
|
||||
const teamTypeRouteRegExp = "/team/:slug/:type((?!book$)[^/]+)";
|
||||
const privateLinkRouteRegExp = "/d/:link/:slug((?!book$)[^/]+)";
|
||||
const embedUserTypeRouteRegExp = `/:user((?!${pages.join("/|")})[^/]*)/:type/embed`;
|
||||
const embedTeamTypeRouteRegExp = "/team/:slug/:type/embed";
|
||||
const subdomainRegExp = getSubdomainRegExp(process.env.NEXT_PUBLIC_WEBAPP_URL);
|
||||
// Important Note: Do update the RegExp in apps/web/test/lib/next-config.test.ts when changing it.
|
||||
const orgHostRegExp = `^(?<orgSlug>${subdomainRegExp})\\..*`;
|
||||
|
||||
const matcherConfigRootPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostRegExp,
|
||||
},
|
||||
],
|
||||
source: "/",
|
||||
};
|
||||
|
||||
const matcherConfigOrgMemberPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: orgHostRegExp,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`,
|
||||
};
|
||||
|
||||
const matcherConfigUserPath = {
|
||||
has: [
|
||||
{
|
||||
type: "host",
|
||||
value: `^(?<orgSlug>${subdomainRegExp}[^.]+)\\..*`,
|
||||
},
|
||||
],
|
||||
source: `/:user((?!${pages.join("|")}|_next|public))/:path*`,
|
||||
};
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
@@ -183,12 +219,26 @@ const nextConfig = {
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
|
||||
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
|
||||
// It would also not match /free/30min/embed because we are ensuring just two slashes
|
||||
const userTypeRouteRegExp = `/:user((?!${pages.join("|")})[^/]*)/:type`;
|
||||
const beforeFiles = [
|
||||
...(process.env.ORGANIZATIONS_ENABLED
|
||||
? [
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
destination: "/team/:orgSlug",
|
||||
},
|
||||
{
|
||||
...matcherConfigOrgMemberPath,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
},
|
||||
{
|
||||
...matcherConfigUserPath,
|
||||
destination: "/:user/:path*",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
let rewrites = [
|
||||
let afterFiles = [
|
||||
{
|
||||
source: "/org/:slug",
|
||||
destination: "/team/:slug",
|
||||
@@ -232,12 +282,12 @@ const nextConfig = {
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: "/team/:slug/:type",
|
||||
source: teamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: "/d/:link/:slug",
|
||||
source: privateLinkRouteRegExp,
|
||||
destination: "/new-booker/d/:link/:slug",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
@@ -245,12 +295,12 @@ const nextConfig = {
|
||||
// Keep cookie based booker enabled to test new-booker embed in production
|
||||
...[
|
||||
{
|
||||
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||
source: embedUserTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type/embed",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: "/team/:slug/:type/embed",
|
||||
source: embedTeamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type/embed",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
@@ -268,14 +318,14 @@ const nextConfig = {
|
||||
// Enable New Booker for all Embed Requests
|
||||
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
|
||||
console.log("Enabling New Booker for Embed");
|
||||
rewrites.push(
|
||||
afterFiles.push(
|
||||
...[
|
||||
{
|
||||
source: `/:user((?!${pages.join("|")}).*)/:type/embed`,
|
||||
source: embedUserTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type/embed",
|
||||
},
|
||||
{
|
||||
source: "/team/:slug/:type/embed",
|
||||
source: embedTeamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type/embed",
|
||||
},
|
||||
]
|
||||
@@ -285,24 +335,27 @@ const nextConfig = {
|
||||
// Enable New Booker for All but embed Requests
|
||||
if (process.env.NEW_BOOKER_ENABLED_FOR_NON_EMBED === "1") {
|
||||
console.log("Enabling New Booker for Non-Embed");
|
||||
rewrites.push(
|
||||
afterFiles.push(
|
||||
...[
|
||||
{
|
||||
source: userTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type",
|
||||
},
|
||||
{
|
||||
source: "/team/:slug/:type",
|
||||
source: teamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type",
|
||||
},
|
||||
{
|
||||
source: "/d/:link/:slug",
|
||||
source: privateLinkRouteRegExp,
|
||||
destination: "/new-booker/d/:link/:slug",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
return rewrites;
|
||||
return {
|
||||
beforeFiles,
|
||||
afterFiles,
|
||||
};
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
@@ -337,6 +390,35 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
...[
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/team/:orgSlug",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigOrgMemberPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/org/:orgSlug/:user",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...matcherConfigUserPath,
|
||||
headers: [
|
||||
{
|
||||
key: "X-Cal-Org-path",
|
||||
value: "/:user/:path",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
@@ -411,6 +493,16 @@ const nextConfig = {
|
||||
destination: "/event-types?openIntercom=true",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/categories/video",
|
||||
destination: "/apps/categories/conferencing",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/apps/installed/video",
|
||||
destination: "/apps/installed/conferencing",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -48,7 +48,7 @@
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^1.0.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-id": "^1.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
"@radix-ui/react-radio-group": "^1.0.0",
|
||||
@@ -61,6 +61,8 @@
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@tremor/react": "^2.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@upstash/ratelimit": "^0.4.3",
|
||||
"@upstash/redis": "^1.21.0",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.5.0",
|
||||
@@ -86,12 +88,12 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.2.1",
|
||||
"next": "^13.4.6",
|
||||
"next-auth": "^4.20.1",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^11.3.0",
|
||||
"next-seo": "^4.26.0",
|
||||
"next-i18next": "^13.2.2",
|
||||
"next-seo": "^6.0.0",
|
||||
"next-themes": "^0.2.0",
|
||||
"nodemailer": "^6.7.8",
|
||||
"otplib": "^12.0.1",
|
||||
@@ -107,7 +109,7 @@
|
||||
"react-hook-form": "^7.43.3",
|
||||
"react-hot-toast": "^2.3.0",
|
||||
"react-intl": "^5.25.1",
|
||||
"react-live-chat-loader": "^2.7.3",
|
||||
"react-live-chat-loader": "^2.8.1",
|
||||
"react-multi-email": "^0.5.3",
|
||||
"react-phone-input-2": "^2.15.1",
|
||||
"react-phone-number-input": "^3.2.7",
|
||||
@@ -160,7 +162,6 @@
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"detect-port": "^1.3.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"module-alias": "^2.2.2",
|
||||
"msw": "^0.42.3",
|
||||
"postcss": "^8.4.18",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
const glob = require("glob");
|
||||
|
||||
/** Needed to rewrite public booking page, gets all static pages but [user] */
|
||||
const pages = glob
|
||||
.sync("pages/**/[^_]*.{tsx,js,ts}", { cwd: __dirname })
|
||||
.map((filename) =>
|
||||
filename
|
||||
.substr(6)
|
||||
.replace(/(\.tsx|\.js|\.ts)/, "")
|
||||
.replace(/\/.*/, "")
|
||||
)
|
||||
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]"));
|
||||
|
||||
exports.pages = pages;
|
||||
+112
-50
@@ -3,22 +3,37 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DOCS_URL, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { BookOpen, Check, ChevronRight, FileText, Slack } from "@calcom/ui/components/icon";
|
||||
import { BookOpen, Check, ChevronRight, FileText, Shield } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
enum pageType {
|
||||
ORG = "org",
|
||||
TEAM = "team",
|
||||
USER = "user",
|
||||
OTHER = "other",
|
||||
}
|
||||
|
||||
export default function Custom404() {
|
||||
const { t } = useLocale();
|
||||
|
||||
const router = useRouter();
|
||||
const [username] = router.asPath.replace("%20", "-").split(/[?#]/);
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [currentPageType, setCurrentPageType] = useState<pageType>(pageType.USER);
|
||||
|
||||
const links = [
|
||||
{
|
||||
title: "Enterprise",
|
||||
description: "Learn more about organizations and subdomains in our enterprise plan.",
|
||||
icon: Shield,
|
||||
href: `${WEBSITE_URL}/enterprise`,
|
||||
},
|
||||
{
|
||||
title: t("documentation"),
|
||||
description: t("documentation_description"),
|
||||
@@ -33,10 +48,29 @@ export default function Custom404() {
|
||||
},
|
||||
];
|
||||
|
||||
const [url, setUrl] = useState(`${WEBSITE_URL}/signup?username=`);
|
||||
const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
|
||||
useEffect(() => {
|
||||
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
|
||||
}, [username]);
|
||||
const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(window.location.host);
|
||||
const [routerUsername] = router.asPath.replace("%20", "-").split(/[?#]/);
|
||||
if (!isValidOrgDomain || !currentOrgDomain) {
|
||||
const splitPath = routerUsername.split("/");
|
||||
if (splitPath[1] === "team" && splitPath.length === 3) {
|
||||
// Accessing a non-existent team
|
||||
setUsername(splitPath[2]);
|
||||
setCurrentPageType(pageType.TEAM);
|
||||
setUrl(`${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${username.replace("/", "")}`);
|
||||
} else {
|
||||
setUsername(routerUsername);
|
||||
setUrl(`${WEBSITE_URL}/signup?username=${username.replace("/", "")}`);
|
||||
}
|
||||
} else {
|
||||
setUsername(currentOrgDomain);
|
||||
setCurrentPageType(pageType.ORG);
|
||||
setUrl(
|
||||
`${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${username.replace("/", "")}`
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isSuccessPage = router.asPath.startsWith("/booking");
|
||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||
@@ -59,7 +93,7 @@ export default function Custom404() {
|
||||
}}
|
||||
/>
|
||||
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||
@@ -80,6 +114,8 @@ export default function Custom404() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!username) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
@@ -91,7 +127,7 @@ export default function Custom404() {
|
||||
}}
|
||||
/>
|
||||
<div className="bg-default min-h-screen px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||
<main className="mx-auto max-w-xl pb-6 pt-16 sm:pt-24">
|
||||
{isSignup && process.env.NEXT_PUBLIC_WEBAPP_URL !== "https://app.cal.com" ? (
|
||||
<div>
|
||||
<div>
|
||||
@@ -161,18 +197,25 @@ export default function Custom404() {
|
||||
</li>
|
||||
<li className="px-4 py-2">
|
||||
<a
|
||||
href={JOIN_SLACK}
|
||||
href={JOIN_DISCORD}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Slack strokeWidth="1.5" fill="currentColor" className="h-6 w-6" />
|
||||
<svg
|
||||
className="text-default h-6 w-6"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 127.14 96.36">
|
||||
<title>Discord Logo</title>
|
||||
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
Slack
|
||||
Discord
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||
@@ -202,13 +245,13 @@ export default function Custom404() {
|
||||
<h1 className="font-cal text-emphasis mt-2 text-4xl font-extrabold sm:text-5xl">
|
||||
{isSuccessPage ? "Booking not found" : t("page_doesnt_exist")}
|
||||
</h1>
|
||||
{isSubpage ? (
|
||||
{isSubpage && currentPageType !== pageType.TEAM ? (
|
||||
<span className="mt-2 inline-block text-lg ">
|
||||
{t("check_spelling_mistakes_or_go_back")}
|
||||
</span>
|
||||
) : isCalcom ? (
|
||||
<a target="_blank" href={url} className="mt-2 inline-block text-lg" rel="noreferrer">
|
||||
{t("the_username")}{" "}
|
||||
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
|
||||
<strong className="text-blue-500">
|
||||
{new URL(WEBSITE_URL).hostname}
|
||||
{username}
|
||||
@@ -217,18 +260,17 @@ export default function Custom404() {
|
||||
</a>
|
||||
) : (
|
||||
<span className="mt-2 inline-block text-lg">
|
||||
{t("the_username")}{" "}
|
||||
{t(`404_the_${currentPageType.toLowerCase()}`)}{" "}
|
||||
<strong className="text-lgtext-green-500 mt-2 inline-block">{username}</strong>{" "}
|
||||
{t("is_still_available")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
|
||||
{t("popular_pages")}
|
||||
</h2>
|
||||
{!isSubpage && isCalcom && (
|
||||
<ul role="list" className="mt-4">
|
||||
{((!isSubpage && isCalcom) ||
|
||||
currentPageType === pageType.ORG ||
|
||||
currentPageType === pageType.TEAM) && (
|
||||
<ul role="list" className="my-4">
|
||||
<li className="border-2 border-green-500 px-4 py-2">
|
||||
<a
|
||||
href={url}
|
||||
@@ -245,11 +287,20 @@ export default function Custom404() {
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("register")} <strong className="text-green-500">{username}</strong>
|
||||
{t("register")}{" "}
|
||||
<strong className="text-green-500">{`${
|
||||
currentPageType === pageType.TEAM
|
||||
? `${new URL(WEBSITE_URL).host}/team/`
|
||||
: ""
|
||||
}${username}${
|
||||
currentPageType === pageType.ORG ? `.${subdomainSuffix()}` : ""
|
||||
}`}</strong>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
|
||||
<p className="text-subtle text-base">
|
||||
{t(`404_claim_entity_${currentPageType.toLowerCase()}`)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
@@ -258,47 +309,58 @@ export default function Custom404() {
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<ul role="list" className="border-subtle divide-subtle mt-4 divide-y">
|
||||
{links.map((link, linkIdx) => (
|
||||
<li key={linkIdx} className="px-4 py-2">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<link.icon className="text-default h-6 w-6" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
<h2 className="text-subtle text-sm font-semibold uppercase tracking-wide">
|
||||
{t("popular_pages")}
|
||||
</h2>
|
||||
<ul role="list" className="border-subtle divide-subtle divide-y">
|
||||
{links
|
||||
.filter((_, idx) => currentPageType === pageType.ORG || idx !== 0)
|
||||
.map((link, linkIdx) => (
|
||||
<li key={linkIdx} className="px-4 py-2">
|
||||
<Link
|
||||
href={link.href}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<link.icon className="text-default h-6 w-6" aria-hidden="true" />
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li className="px-4 py-2">
|
||||
<a
|
||||
href={JOIN_SLACK}
|
||||
href={JOIN_DISCORD}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Slack strokeWidth="1.5" fill="currentColor" className="h-6 w-6" />
|
||||
<svg
|
||||
className="text-default h-6 w-6"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 127.14 96.36">
|
||||
<title>Discord Logo</title>
|
||||
<path d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z" />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-emphasis text-base font-medium">
|
||||
<span className="focus-within:ring-empthasis rounded-sm focus-within:ring-2 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
Slack
|
||||
Discord
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function Error500() {
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">500</h1>
|
||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">It's not you, it's us.</h2>
|
||||
<p className="text-default mt-4 mb-6 max-w-2xl text-sm">
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
|
||||
away for you.
|
||||
</p>
|
||||
|
||||
@@ -37,7 +37,8 @@ import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||
export type UserPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||
export function UserPage(props: UserPageProps) {
|
||||
const {
|
||||
users,
|
||||
profile,
|
||||
@@ -102,6 +103,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||
const query = { ...router.query };
|
||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||
delete query.orgSlug;
|
||||
const nameOrUsername = user.name || user.username || "";
|
||||
|
||||
/*
|
||||
@@ -132,7 +134,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "mx-auto" : "",
|
||||
isEmbed ? "border-booker border-booker-width bg-default rounded-md border" : "",
|
||||
"max-w-3xl py-24 px-4"
|
||||
"max-w-3xl px-4 py-24"
|
||||
)}>
|
||||
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
||||
<div className="mb-8 text-center">
|
||||
@@ -207,8 +209,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
);
|
||||
}
|
||||
|
||||
User.isBookingPage = true;
|
||||
User.PageWrapper = PageWrapper;
|
||||
UserPage.isBookingPage = true;
|
||||
UserPage.PageWrapper = PageWrapper;
|
||||
|
||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||
return (
|
||||
@@ -360,6 +362,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
const markdownStrippedBio = stripMarkdown(user?.bio || "");
|
||||
|
||||
return {
|
||||
props: {
|
||||
users,
|
||||
@@ -385,3 +388,5 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default UserPage;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { getStaticProps, getStaticPaths, default } from "./index";
|
||||
@@ -1,69 +0,0 @@
|
||||
/**
|
||||
* This page is empty for the user, it is used only to take advantage of the
|
||||
* caching system that NextJS uses SSG pages.
|
||||
* TODO: Redirect to user profile on browser
|
||||
*/
|
||||
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import getCalendarsEvents from "@calcom/core/getCalendarsEvents";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const paramsSchema = z.object({ user: z.string(), month: z.string() });
|
||||
export const getStaticProps: GetStaticProps<
|
||||
{ results: Awaited<ReturnType<typeof getCalendarsEvents>> },
|
||||
{ user: string }
|
||||
> = async (context) => {
|
||||
const { user: username, month } = paramsSchema.parse(context.params);
|
||||
const userWithCredentials = await prisma.user.findFirst({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
credentials: true,
|
||||
selectedCalendars: true,
|
||||
},
|
||||
});
|
||||
// Subtract 11 hours from the start date to avoid problems in UTC- time zones.
|
||||
const startDate = dayjs.utc(month, "YYYY-MM").startOf("day").subtract(11, "hours").format();
|
||||
// Add 14 hours from the start date to avoid problems in UTC+ time zones.
|
||||
const endDate = dayjs.utc(month, "YYYY-MM").endOf("month").add(14, "hours").format();
|
||||
try {
|
||||
const results = userWithCredentials?.credentials
|
||||
? await getCalendarsEvents(
|
||||
userWithCredentials?.credentials,
|
||||
startDate,
|
||||
endDate,
|
||||
userWithCredentials?.selectedCalendars
|
||||
)
|
||||
: [];
|
||||
|
||||
return {
|
||||
props: { results, date: new Date().toISOString() },
|
||||
revalidate: 1,
|
||||
};
|
||||
} catch (error) {
|
||||
let message = "Unknown error while fetching calendar";
|
||||
if (error instanceof Error) message = error.message;
|
||||
console.error(error, message);
|
||||
return {
|
||||
props: { results: [], date: new Date().toISOString(), message },
|
||||
revalidate: 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
type Props = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
const CalendarCache = (props: Props) =>
|
||||
process.env.NODE_ENV === "development" ? <pre>{JSON.stringify(props, null, " ")}</pre> : <div />;
|
||||
|
||||
export default CalendarCache;
|
||||
@@ -5,15 +5,11 @@ import { z } from "zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendPasswordResetEmail } from "@calcom/emails";
|
||||
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const limiter = rateLimit({
|
||||
intervalInMs: 60 * 1000, // 1 minute
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||
|
||||
@@ -37,11 +33,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
// 10 requests per minute
|
||||
|
||||
try {
|
||||
limiter.check(10, ip);
|
||||
} catch (e) {
|
||||
return res.status(429).json({ message: "Too Many Requests." });
|
||||
}
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: ip,
|
||||
});
|
||||
|
||||
try {
|
||||
const maybeUser = await prisma.user.findUnique({
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { sendEmailVerification } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
const signupSchema = z.object({
|
||||
username: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(7),
|
||||
language: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -26,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
const data = req.body;
|
||||
const { email, password, language } = signupSchema.parse(data);
|
||||
const { email, password, language, token } = signupSchema.parse(data);
|
||||
|
||||
const username = slugify(data.username);
|
||||
const userEmail = email.toLowerCase();
|
||||
@@ -85,25 +88,100 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
|
||||
// If user has been invitedTo a team, we accept the membership
|
||||
if (user.invitedTo) {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: { id: user.invitedTo },
|
||||
if (token) {
|
||||
const foundToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
token,
|
||||
},
|
||||
});
|
||||
|
||||
if (team) {
|
||||
const membership = await prisma.membership.update({
|
||||
if (!foundToken) {
|
||||
return res.status(401).json({ message: "Invalid Token" });
|
||||
}
|
||||
|
||||
if (dayjs(foundToken?.expires).isBefore(dayjs())) {
|
||||
return res.status(401).json({ message: "Token expired" });
|
||||
}
|
||||
|
||||
if (foundToken.teamId) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
userId_teamId: { userId: user.id, teamId: user.invitedTo },
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
id: foundToken.teamId,
|
||||
},
|
||||
});
|
||||
|
||||
// Sync Services: Close.com
|
||||
closeComUpsertTeamUser(team, user, membership.role);
|
||||
if (team) {
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
if (teamMetadata?.isOrganization) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
organizationId: team.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const membership = await prisma.membership.update({
|
||||
where: {
|
||||
userId_teamId: { userId: user.id, teamId: team.id },
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
closeComUpsertTeamUser(team, user, membership.role);
|
||||
|
||||
// Accept any child team invites for orgs.
|
||||
if (team.parentId) {
|
||||
// Join ORG
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
organizationId: team.parentId,
|
||||
},
|
||||
});
|
||||
|
||||
/** We do a membership update twice so we can join the ORG invite if the user is invited to a team witin a ORG. */
|
||||
await prisma.membership.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
team: {
|
||||
id: team.parentId,
|
||||
},
|
||||
accepted: false,
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Join any other invites
|
||||
await prisma.membership.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
team: {
|
||||
parentId: team.parentId,
|
||||
},
|
||||
accepted: false,
|
||||
},
|
||||
data: {
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup token after use
|
||||
await prisma.verificationToken.delete({
|
||||
where: {
|
||||
id: foundToken.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await sendEmailVerification({
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const selectedCalendarSelectSchema = z.object({
|
||||
integration: z.string(),
|
||||
externalId: z.string(),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
@@ -32,18 +37,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const { credentials, ...user } = userWithCredentials;
|
||||
|
||||
if (req.method === "POST") {
|
||||
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.body);
|
||||
await prisma.selectedCalendar.upsert({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId: user.id,
|
||||
integration: req.body.integration,
|
||||
externalId: req.body.externalId,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
userId: user.id,
|
||||
integration: req.body.integration,
|
||||
externalId: req.body.externalId,
|
||||
integration,
|
||||
externalId,
|
||||
},
|
||||
// already exists
|
||||
update: {},
|
||||
@@ -52,12 +58,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
if (req.method === "DELETE") {
|
||||
const { integration, externalId } = selectedCalendarSelectSchema.parse(req.query);
|
||||
await prisma.selectedCalendar.delete({
|
||||
where: {
|
||||
userId_integration_externalId: {
|
||||
userId: user.id,
|
||||
externalId: req.body.externalId,
|
||||
integration: req.body.integration,
|
||||
externalId,
|
||||
integration,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,10 +72,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
res.status(200).json({ message: "Calendar Selection Saved" });
|
||||
}
|
||||
|
||||
if (["DELETE", "POST"].includes(req.method)) {
|
||||
await revalidateCalendarCache(res.revalidate, `${session?.user?.username}`);
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
const selectedCalendarIds = await prisma.selectedCalendar.findMany({
|
||||
where: {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { AppCategories, Prisma } from "@calcom/prisma/client";
|
||||
|
||||
const isDryRun = process.env.CRON_ENABLE_APP_SYNC !== "true";
|
||||
const log = logger.getChildLogger({
|
||||
prefix: ["[api/cron/syncAppMeta]", ...(isDryRun ? ["(dry-run)"] : [])],
|
||||
});
|
||||
|
||||
/**
|
||||
* syncAppMeta makes sure any app metadata that has been replicated into the database
|
||||
* remains synchronized with any changes made to the app config files.
|
||||
*/
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
if (req.method !== "POST") {
|
||||
res.status(405).json({ message: "Invalid method" });
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`🧐 Checking DB apps are in-sync with app metadata`);
|
||||
|
||||
const dbApps = await prisma.app.findMany();
|
||||
|
||||
for await (const dbApp of dbApps) {
|
||||
const app = await getAppWithMetadata(dbApp);
|
||||
const updates: Prisma.AppUpdateManyMutationInput = {};
|
||||
|
||||
if (!app) {
|
||||
log.warn(`💀 App ${dbApp.slug} (${dbApp.dirName}) no longer exists.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for any changes in the app categories (tolerates changes in ordering)
|
||||
if (
|
||||
dbApp.categories.length !== app.categories.length ||
|
||||
!dbApp.categories.every((category) => app.categories.includes(category))
|
||||
) {
|
||||
updates["categories"] = app.categories as AppCategories[];
|
||||
}
|
||||
|
||||
if (dbApp.dirName !== (app.dirName ?? app.slug)) {
|
||||
updates["dirName"] = app.dirName ?? app.slug;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
log.info(`🔨 Updating app ${dbApp.slug} with ${Object.keys(updates).join(", ")}`);
|
||||
if (!isDryRun) {
|
||||
await prisma.app.update({
|
||||
where: { slug: dbApp.slug },
|
||||
data: updates,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.info(`✅ App ${dbApp.slug} is up-to-date and correct`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
@@ -62,14 +61,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
if (typeof handler === "function") {
|
||||
await handler(req, res);
|
||||
if (appName.includes("calendar") && req.session?.user?.username) {
|
||||
await revalidateCalendarCache(res.revalidate, req.session?.user?.username);
|
||||
}
|
||||
} else {
|
||||
await defaultIntegrationAddHandler({ user: req.session?.user, ...handler });
|
||||
if (handler.appType.includes("calendar") && req.session?.user?.username) {
|
||||
await revalidateCalendarCache(res.revalidate, req.session?.user?.username);
|
||||
}
|
||||
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
|
||||
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
|
||||
const querySchema = z.object({
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
* This endpoint revalidates users calendar cache several months ahead
|
||||
* Can be used as webhook
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { username } = querySchema.parse(req.query);
|
||||
try {
|
||||
await revalidateCalendarCache(res.revalidate, username);
|
||||
|
||||
return res.status(200).json({ revalidated: true });
|
||||
} catch (err) {
|
||||
return res.status(500).send({ message: "Error revalidating" });
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationTypeFromApp } from "@calcom/app-store/locations";
|
||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
@@ -29,11 +29,14 @@ import {
|
||||
DropdownItem,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import type { LucideIcon } from "@calcom/ui/components/icon";
|
||||
import {
|
||||
BarChart,
|
||||
Calendar,
|
||||
Contact,
|
||||
CreditCard,
|
||||
Grid,
|
||||
Mail,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Share2,
|
||||
@@ -101,8 +104,8 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
}
|
||||
|
||||
interface IntegrationsContainerProps {
|
||||
variant?: (typeof InstalledAppVariants)[number];
|
||||
exclude?: (typeof InstalledAppVariants)[number][];
|
||||
variant?: AppCategories;
|
||||
exclude?: AppCategories[];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
}
|
||||
|
||||
@@ -225,14 +228,19 @@ const IntegrationsContainer = ({
|
||||
}: IntegrationsContainerProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
||||
const emptyIcon = {
|
||||
|
||||
// TODO: Refactor and reuse getAppCategories?
|
||||
const emptyIcon: Record<AppCategories, LucideIcon> = {
|
||||
calendar: Calendar,
|
||||
conferencing: Video,
|
||||
automation: Share2,
|
||||
analytics: BarChart,
|
||||
payment: CreditCard,
|
||||
web3: BarChart,
|
||||
web3: BarChart, // deprecated
|
||||
other: Grid,
|
||||
video: Video, // deprecated
|
||||
messaging: Mail,
|
||||
crm: Contact,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -267,9 +275,7 @@ const IntegrationsContainer = ({
|
||||
className="mb-6"
|
||||
actions={
|
||||
<Button
|
||||
href={
|
||||
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
|
||||
}
|
||||
href={variant ? `/apps/categories/${variant}` : "/apps"}
|
||||
color="secondary"
|
||||
StartIcon={Plus}>
|
||||
{t("add")}
|
||||
@@ -285,7 +291,7 @@ const IntegrationsContainer = ({
|
||||
};
|
||||
|
||||
const querySchema = z.object({
|
||||
category: z.enum(InstalledAppVariants),
|
||||
category: z.nativeEnum(AppCategories),
|
||||
});
|
||||
|
||||
type querySchemaType = z.infer<typeof querySchema>;
|
||||
@@ -299,13 +305,11 @@ export default function InstalledApps() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const category = router.query.category as querySchemaType["category"];
|
||||
const categoryList: querySchemaType["category"][] = [
|
||||
"payment",
|
||||
"conferencing",
|
||||
"automation",
|
||||
"analytics",
|
||||
"web3",
|
||||
];
|
||||
|
||||
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
|
||||
// Exclude calendar and other from categoryList, we handle those slightly differently below
|
||||
return !(category in { other: null, calendar: null });
|
||||
});
|
||||
|
||||
const [data, updateData] = useReducer(
|
||||
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetStaticPropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import type { ReactElement } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -21,7 +22,7 @@ export default function Error() {
|
||||
const router = useRouter();
|
||||
const { error } = querySchema.parse(router.query);
|
||||
const isTokenVerificationError = error?.toLowerCase() === "verification";
|
||||
let errorMsg = <SkeletonText />;
|
||||
let errorMsg: string | ReactElement = <SkeletonText />;
|
||||
if (router.isReady) {
|
||||
errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
|
||||
}
|
||||
|
||||
@@ -189,7 +189,11 @@ export default function Availability() {
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 sm:flex">
|
||||
<Skeleton as={Label} htmlFor="hiddenSwitch" className="mt-2 cursor-pointer self-center pr-2 ">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pe-2"
|
||||
loadingClassName="me-4">
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
@@ -254,7 +258,7 @@ export default function Availability() {
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="flex h-9 flex-row items-center justify-between py-2 px-4 hover:bg-gray-100">
|
||||
<div className="flex h-9 flex-row items-center justify-between px-4 py-2 hover:bg-gray-100">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
@@ -315,9 +319,9 @@ export default function Availability() {
|
||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||
<div className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="text-default block text-sm font-medium">
|
||||
<Skeleton as={Label} htmlFor="timeZone" className="mb-0 inline-block leading-none">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
</Skeleton>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
@@ -328,18 +332,20 @@ export default function Availability() {
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="w-72" />
|
||||
<SelectSkeletonLoader className="mt-1 w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className="border-subtle my-6 mr-8" />
|
||||
<div className="hidden rounded-md md:block">
|
||||
<h3 className="text-emphasis text-sm font-medium">{t("something_doesnt_look_right")}</h3>
|
||||
<Skeleton as="h3" className="mb-0 inline-block text-sm font-medium">
|
||||
{t("something_doesnt_look_right")}
|
||||
</Skeleton>
|
||||
<div className="mt-3 flex">
|
||||
<Button href="/availability/troubleshoot" color="secondary">
|
||||
<Skeleton as={Button} href="/availability/troubleshoot" color="secondary">
|
||||
{t("launch_troubleshooter")}
|
||||
</Button>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -20,6 +21,8 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
|
||||
const meQuery = trpc.viewer.me.useQuery();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
|
||||
onMutate: async ({ scheduleId }) => {
|
||||
await utils.viewer.availability.list.cancel();
|
||||
@@ -67,6 +70,19 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push(`/availability/${schedule.id}`);
|
||||
showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Adds smooth delete button - item fades and old item slides into place
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||
@@ -96,6 +112,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
isDeletable={schedules.length !== 1}
|
||||
updateDefault={updateMutation.mutate}
|
||||
deleteFunction={deleteMutation.mutate}
|
||||
duplicateFunction={duplicateMutation.mutate}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -49,8 +49,9 @@ import type { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme } from "@calcom/ui";
|
||||
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme, Alert } from "@calcom/ui";
|
||||
import { X, ExternalLink, ChevronLeft, Check, Calendar } from "@calcom/ui/components/icon";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone } from "@lib/clock";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -127,6 +128,10 @@ export default function Success(props: SuccessProps) {
|
||||
? props?.bookingInfo?.attendees?.[0]?.name
|
||||
: "Nameless";
|
||||
|
||||
const attendees = props?.bookingInfo?.attendees;
|
||||
|
||||
const isGmail = !!attendees.find((attendee) => attendee.email.includes("gmail.com"));
|
||||
|
||||
const [is24h, setIs24h] = useState(isBrowserLocale24h());
|
||||
const { data: session } = useSession();
|
||||
|
||||
@@ -306,7 +311,7 @@ export default function Success(props: SuccessProps) {
|
||||
/>
|
||||
)}
|
||||
{userIsOwner && !isEmbed && (
|
||||
<div className="mt-2 ml-4 -mb-4">
|
||||
<div className="-mb-4 ml-4 mt-2">
|
||||
<Link
|
||||
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
|
||||
className="hover:bg-subtle text-subtle hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
|
||||
@@ -321,7 +326,7 @@ export default function Success(props: SuccessProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "text-center" : "",
|
||||
"flex items-end justify-center px-4 pt-4 pb-20 sm:block sm:p-0"
|
||||
"flex items-end justify-center px-4 pb-20 pt-4 sm:block sm:p-0"
|
||||
)}>
|
||||
<div
|
||||
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
|
||||
@@ -330,7 +335,7 @@ export default function Success(props: SuccessProps) {
|
||||
className={classNames(
|
||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
|
||||
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||
)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -356,7 +361,7 @@ export default function Success(props: SuccessProps) {
|
||||
{needsConfirmation && !isCancelled && <Calendar className="text-emphasis h-5 w-5" />}
|
||||
{isCancelled && <X className="h-5 w-5 text-red-600" />}
|
||||
</div>
|
||||
<div className="mt-6 mb-8 text-center last:mb-0">
|
||||
<div className="mb-8 mt-6 text-center last:mb-0">
|
||||
<h3
|
||||
className="text-emphasis text-2xl font-semibold leading-6"
|
||||
data-testid={isCancelled ? "cancelled-headline" : ""}
|
||||
@@ -394,7 +399,7 @@ export default function Success(props: SuccessProps) {
|
||||
{(isCancelled || reschedule) && cancellationReason && (
|
||||
<>
|
||||
<div className="font-medium">
|
||||
{isCancelled ? t("reason") : t("reschedule_reason_success_page")}
|
||||
{isCancelled ? t("reason") : t("reschedule_reason")}
|
||||
</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
|
||||
</>
|
||||
@@ -615,7 +620,7 @@ export default function Success(props: SuccessProps) {
|
||||
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
className="-mt-1.5 mr-1 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
@@ -642,7 +647,7 @@ export default function Success(props: SuccessProps) {
|
||||
className="text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
className="-mt-1.5 mr-1 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
@@ -659,7 +664,7 @@ export default function Success(props: SuccessProps) {
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 1000"
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
||||
className="-mt-1.5 mr-1 inline-block h-4 w-4">
|
||||
<title>{t("other")}</title>
|
||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||
</svg>
|
||||
@@ -705,6 +710,26 @@ export default function Success(props: SuccessProps) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isGmail && (
|
||||
<Alert
|
||||
className="main -mb-20 mt-4 inline-block text-left sm:-mt-4 sm:mb-4 sm:w-full sm:max-w-xl sm:align-middle"
|
||||
severity="warning"
|
||||
message={
|
||||
<div>
|
||||
<p className="font-semibold">{t("google_new_spam_policy")}</p>
|
||||
<span className="underline">
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://cal.com/blog/google-s-new-spam-policy-may-be-affecting-your-invitations">
|
||||
{t("resolve")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
CustomIcon={AlertCircle}
|
||||
customIconColor="text-attention dark:text-orange-200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -29,27 +29,54 @@ import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { ConfirmationDialogContent, Dialog, Form, showToast } from "@calcom/ui";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
|
||||
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
|
||||
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
||||
import { EventTeamTab } from "@components/eventtype/EventTeamTab";
|
||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||
import { EventWebhooksTab } from "@components/eventtype/EventWebhooksTab";
|
||||
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
|
||||
const EventSetupTab = dynamic(() =>
|
||||
import("@components/eventtype/EventSetupTab").then((mod) => mod.EventSetupTab)
|
||||
);
|
||||
|
||||
const EventAvailabilityTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAvailabilityTab").then((mod) => mod.EventAvailabilityTab)
|
||||
);
|
||||
|
||||
const EventTeamTab = dynamic(() =>
|
||||
import("@components/eventtype/EventTeamTab").then((mod) => mod.EventTeamTab)
|
||||
);
|
||||
|
||||
const EventLimitsTab = dynamic(() =>
|
||||
import("@components/eventtype/EventLimitsTab").then((mod) => mod.EventLimitsTab)
|
||||
);
|
||||
|
||||
const EventAdvancedTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
|
||||
);
|
||||
|
||||
const EventRecurringTab = dynamic(() =>
|
||||
import("@components/eventtype/EventRecurringTab").then((mod) => mod.EventRecurringTab)
|
||||
);
|
||||
|
||||
const EventAppsTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAppsTab").then((mod) => mod.EventAppsTab)
|
||||
);
|
||||
|
||||
const EventWorkflowsTab = dynamic(() => import("@components/eventtype/EventWorkfowsTab"));
|
||||
|
||||
const EventWebhooksTab = dynamic(() =>
|
||||
import("@components/eventtype/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
|
||||
);
|
||||
|
||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||
|
||||
export type FormValues = {
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
@@ -168,18 +195,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
message = `${err.data.code}: You are not able to update this event`;
|
||||
message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
|
||||
message = `${err.data.code}: ${err.message}`;
|
||||
message = `${err.data.code}: ${t(err.message)}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
showToast(message, "error");
|
||||
} else {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
showToast(message ? t(message) : t(err.message), "error");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -476,46 +499,23 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
</EventTypeSingleLayout>
|
||||
<Dialog
|
||||
open={slugExistsChildrenDialogOpen.length > 0}
|
||||
onOpenChange={() => {
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}>
|
||||
<ConfirmationDialogContent
|
||||
|
||||
{slugExistsChildrenDialogOpen.length && (
|
||||
<ManagedEventTypeDialog
|
||||
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
||||
isLoading={formMethods.formState.isSubmitting}
|
||||
variety="warning"
|
||||
title={t("managed_event_dialog_title", {
|
||||
slug,
|
||||
count: slugExistsChildrenDialogOpen.length,
|
||||
})}
|
||||
confirmBtnText={t("managed_event_dialog_confirm_button", {
|
||||
count: slugExistsChildrenDialogOpen.length,
|
||||
})}
|
||||
cancelBtnText={t("go_back")}
|
||||
onOpenChange={() => {
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}
|
||||
slug={slug}
|
||||
onConfirm={(e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(formMethods.getValues());
|
||||
telemetry.event(telemetryEventTypes.slugReplacementAction);
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey="managed_event_dialog_information"
|
||||
values={{
|
||||
names: `${slugExistsChildrenDialogOpen
|
||||
.map((ch) => ch.owner.name)
|
||||
.slice(0, -1)
|
||||
.join(", ")} ${
|
||||
slugExistsChildrenDialogOpen.length > 1 ? t("and") : ""
|
||||
} ${slugExistsChildrenDialogOpen.map((ch) => ch.owner.name).slice(-1)}`,
|
||||
slug,
|
||||
}}
|
||||
count={slugExistsChildrenDialogOpen.length}
|
||||
/>
|
||||
</p>{" "}
|
||||
<p className="mt-5">{t("managed_event_dialog_clarification")}</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,12 +8,12 @@ import { useEffect, useState, memo } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import { OrganizationEventTypeFilter } from "@calcom/features/eventtypes/components/OrganizationEventTypeFilter";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
HeadSeo,
|
||||
Skeleton,
|
||||
Label,
|
||||
VerticalDivider,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
@@ -371,9 +370,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
|
||||
{types.map((type, index) => {
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${
|
||||
orgBranding ? `${new URL(CAL_URL).protocol}//${orgBranding.slug}.${subdomainSuffix()}` : CAL_URL
|
||||
}/${embedLink}`;
|
||||
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
@@ -383,7 +380,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
||||
{!(firstItem && firstItem.id === type.id) && (
|
||||
<button
|
||||
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -ml-4 -mt-4 mb-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||
onClick={() => moveEventType(index, -1)}>
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -391,7 +388,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
|
||||
{!(lastItem && lastItem.id === type.id) && (
|
||||
<button
|
||||
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] -ml-4 mt-8 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||
onClick={() => moveEventType(index, 1)}>
|
||||
<ArrowDown className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -401,7 +398,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && !isManagedEventType && (
|
||||
<AvatarGroup
|
||||
className="relative top-1 right-3"
|
||||
className="relative right-3 top-1"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={type.users.map(
|
||||
@@ -415,12 +412,12 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
)}
|
||||
{isManagedEventType && (
|
||||
<AvatarGroup
|
||||
className="relative top-1 right-3"
|
||||
className="relative right-3 top-1"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={type.children
|
||||
.flatMap((ch) => ch.users)
|
||||
.map((user: User) => ({
|
||||
.map((user: Pick<User, "name" | "username">) => ({
|
||||
alt: user.name || "",
|
||||
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||
title: user.name || "",
|
||||
@@ -640,7 +637,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{!isManagedEventType && (
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between py-2 px-4">
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
@@ -698,10 +695,10 @@ const EventTypeListHeading = ({
|
||||
profile,
|
||||
membershipCount,
|
||||
teamId,
|
||||
orgSlug,
|
||||
}: EventTypeListHeadingProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const orgBranding = useOrgBrandingValues();
|
||||
|
||||
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
|
||||
onSuccess(data) {
|
||||
@@ -728,10 +725,10 @@ const EventTypeListHeading = ({
|
||||
{profile?.name || ""}
|
||||
</Link>
|
||||
{membershipCount && teamId && (
|
||||
<span className="text-subtle ms-2 me-2 relative -top-px text-xs">
|
||||
<span className="text-subtle relative -top-px me-2 ms-2 text-xs">
|
||||
<Link href={`/settings/teams/${teamId}/members`}>
|
||||
<Badge variant="gray">
|
||||
<Users className="mr-1 -mt-px inline h-3 w-3" />
|
||||
<Users className="-mt-px mr-1 inline h-3 w-3" />
|
||||
{membershipCount}
|
||||
</Badge>
|
||||
</Link>
|
||||
@@ -739,15 +736,15 @@ const EventTypeListHeading = ({
|
||||
)}
|
||||
{profile?.slug && (
|
||||
<Link href={`${CAL_URL}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
{orgSlug
|
||||
? `${orgSlug}.${subdomainSuffix()}/${profile.slug}`
|
||||
: `${CAL_URL?.replace("https://", "")}/${profile.slug}`}
|
||||
{orgBranding
|
||||
? `${orgBranding.fullDomain.replace("https://", "").replace("http://", "")}${profile.slug}`
|
||||
: `${CAL_URL?.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{!profile?.slug && !!teamId && (
|
||||
<button onClick={() => publishTeamMutation.mutate({ teamId })}>
|
||||
<Badge variant="gray" className="mb-1 -ml-2">
|
||||
<Badge variant="gray" className="-ml-2 mb-1">
|
||||
{t("upgrade")}
|
||||
</Badge>
|
||||
</button>
|
||||
@@ -799,9 +796,8 @@ const CTA = () => {
|
||||
|
||||
const Actions = () => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<OrganizationEventTypeFilter />
|
||||
<VerticalDivider />
|
||||
<div className="hidden items-center md:flex">
|
||||
<TeamsFilter popoverTriggerClassNames="mb-0" showVerticalDivider={true} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -834,9 +830,6 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any);
|
||||
|
||||
const EventTypesPage = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -862,6 +855,10 @@ const EventTypesPage = () => {
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const filters = getTeamsFiltersFromQuery(router.query);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer as any, filters && { filters });
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -879,47 +876,55 @@ const EventTypesPage = () => {
|
||||
CTA={<CTA />}>
|
||||
<WithQuery
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 ? (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
orgSlug={orgBranding?.slug}
|
||||
/>
|
||||
success={({ data }) => {
|
||||
const isFilteredByOnlyOneItem =
|
||||
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
|
||||
data.eventTypeGroups.length === 1;
|
||||
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : data.eventTypeGroups.length === 1 ? (
|
||||
<EventTypeList
|
||||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
) : (
|
||||
<CreateFirstEventTypeView />
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
|
||||
<div className="flex flex-col" key={group.profile.slug}>
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
orgSlug={orgBranding?.slug}
|
||||
/>
|
||||
|
||||
<EmbedDialog />
|
||||
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
)}
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data.eventTypeGroups.length === 1 && (
|
||||
<EventTypeList
|
||||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
|
||||
|
||||
<EmbedDialog />
|
||||
{router.query.dialog === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Shell>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ const Heading = () => {
|
||||
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 text-emphasis truncate text-xl font-semibold tracking-wide xl:max-w-full">
|
||||
{t("insights")}
|
||||
</h3>
|
||||
<p className="text-default hidden text-sm md:block">{t("subtitle_analytics")}</p>
|
||||
<p className="text-default mt-2 hidden text-sm md:block">{t("insights_subtitle")}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -35,6 +35,7 @@ const Heading = () => {
|
||||
export default function InsightsPage() {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
@@ -77,10 +78,12 @@ export default function InsightsPage() {
|
||||
<></>
|
||||
) : (
|
||||
<FiltersProvider>
|
||||
<div className="mb-8 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
|
||||
<div className="ml-auto mt-0">
|
||||
<Heading />
|
||||
<Filters />
|
||||
</div>
|
||||
|
||||
<Filters />
|
||||
|
||||
<div className="mb-4 space-y-6">
|
||||
<BookingKPICards />
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function MaintenancePage() {
|
||||
</Head>
|
||||
<div className="bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="text-emphasis text-2xl font-medium">{t("under_maintenance")}</h1>
|
||||
<p className="text-default mt-4 mb-6 max-w-2xl text-sm">
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
{t("under_maintenance_description", { appName: APP_NAME })}
|
||||
</p>
|
||||
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -40,17 +41,16 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
const { user, type: slug } = paramsSchema.parse(context.params);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const { rescheduleUid } = context.query;
|
||||
|
||||
const { ssrInit } = await import("@server/lib/ssr");
|
||||
const ssr = await ssrInit(context);
|
||||
const usernameList = getUsernameList(user);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
username: {
|
||||
in: usernameList,
|
||||
in: usernames,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
@@ -71,7 +71,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({ username: user, eventSlug: slug });
|
||||
const eventData = await ssr.viewer.public.event.fetch({ username: usernames.join("+"), eventSlug: slug });
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
@@ -82,7 +82,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
user,
|
||||
user: usernames.join("+"),
|
||||
slug,
|
||||
away: false,
|
||||
trpcState: ssr.dehydrate(),
|
||||
@@ -93,7 +93,8 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
|
||||
}
|
||||
|
||||
async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
const { user: username, type: slug } = paramsSchema.parse(context.params);
|
||||
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
|
||||
const username = usernames[0];
|
||||
const { rescheduleUid } = context.query;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
|
||||
|
||||
@@ -148,13 +149,16 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
user: z.string().transform((s) => getUsernameList(s)),
|
||||
});
|
||||
|
||||
// Booker page fetches a tiny bit of data server side, to determine early
|
||||
// whether the page should show an away state or dynamic booking not allowed.
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { user } = paramsSchema.parse(context.params);
|
||||
const isDynamicGroup = getUsernameList(user).length > 1;
|
||||
const isDynamicGroup = user.length > 1;
|
||||
|
||||
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -14,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
|
||||
export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent }: PageProps) {
|
||||
return (
|
||||
<main className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<BookerSeo
|
||||
@@ -29,6 +30,7 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
|
||||
rescheduleBooking={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent={isTeamEvent}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
@@ -57,6 +59,11 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -96,9 +103,11 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
|
||||
}
|
||||
|
||||
const isTeamEvent = !!hashedLink.eventType?.team?.id;
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug });
|
||||
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent });
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
@@ -114,11 +123,14 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
|
||||
slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isBrandingHidden: user?.hideBranding,
|
||||
// Sending the team event from the server, because this template file
|
||||
// is reused for both team and user events.
|
||||
isTeamEvent,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const paramsSchema = z.object({ link: z.string(), slug: z.string() });
|
||||
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
|
||||
|
||||
// Booker page fetches a tiny bit of data server side, to determine early
|
||||
// whether the page should show an away state or dynamic booking not allowed.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
|
||||
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -23,6 +24,7 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
|
||||
eventSlug={slug}
|
||||
rescheduleUid={booking?.uid}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
/>
|
||||
<Booker
|
||||
username={user}
|
||||
@@ -30,6 +32,7 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
|
||||
rescheduleBooking={booking}
|
||||
isAway={away}
|
||||
hideBranding={isBrandingHidden}
|
||||
isTeamEvent
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
@@ -37,7 +40,10 @@ export default function Type({ slug, user, booking, away, isBrandingHidden }: Pa
|
||||
|
||||
Type.PageWrapper = PageWrapper;
|
||||
|
||||
const paramsSchema = z.object({ type: z.string(), slug: z.string() });
|
||||
const paramsSchema = z.object({
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
// Booker page fetches a tiny bit of data server side:
|
||||
// 1. Check if team exists, to show 404
|
||||
@@ -71,7 +77,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
// We use this to both prefetch the query on the server,
|
||||
// as well as to check if the event exist, so we c an show a 404 otherwise.
|
||||
const eventData = await ssr.viewer.public.event.fetch({ username: teamSlug, eventSlug: meetingSlug });
|
||||
const eventData = await ssr.viewer.public.event.fetch({
|
||||
username: teamSlug,
|
||||
eventSlug: meetingSlug,
|
||||
isTeamEvent: true,
|
||||
});
|
||||
|
||||
if (!eventData) {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "../../../[user]/[type]";
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { UserPageProps } from "../../../[user]";
|
||||
import UserPage, { getServerSideProps as GSSUserPage } from "../../../[user]";
|
||||
import type { TeamPageProps } from "../../../team/[slug]";
|
||||
import TeamPage, { getServerSideProps as GSSTeamPage } from "../../../team/[slug]";
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: ctx.query.user as string,
|
||||
parentId: {
|
||||
not: null,
|
||||
},
|
||||
parent: {
|
||||
slug: ctx.query.orgSlug as string,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
if (team) {
|
||||
return GSSTeamPage({ ...ctx, query: { slug: ctx.query.user } });
|
||||
}
|
||||
return GSSUserPage({ ...ctx, query: { user: ctx.query.user } });
|
||||
};
|
||||
|
||||
type Props = UserPageProps | TeamPageProps;
|
||||
|
||||
export default function Page(props: Props) {
|
||||
if ((props as TeamPageProps)?.team) return TeamPage(props as TeamPageProps);
|
||||
return UserPage(props as UserPageProps);
|
||||
}
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "../../team/[slug]";
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "../../../../team/[slug]/[type]";
|
||||
@@ -0,0 +1 @@
|
||||
export { getServerSideProps, default } from "../../../../team/[slug]";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user