Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
761dfac033 | ||
|
|
9fda4acc5b | ||
|
|
0470708aad | ||
|
|
bee392ba1a | ||
|
|
448f57442b | ||
|
|
856b090b4b | ||
|
|
b99661a411 | ||
|
|
d96a26eb3c | ||
|
|
9d08f5c7c8 | ||
|
|
4fe5f6ca6f | ||
|
|
af505935bd | ||
|
|
4a30f60622 | ||
|
|
24ed04e6c7 | ||
|
|
bed3595314 | ||
|
|
74671263bf | ||
|
|
bdc5e9c7e9 | ||
|
|
8fa1c70a97 | ||
|
|
1343c8fa8d | ||
|
|
5c0f9510a1 | ||
|
|
b2801170fa | ||
|
|
6ac19e8307 | ||
|
|
344241cfbf | ||
|
|
66ebf18fa9 | ||
|
|
e6d6f3317e | ||
|
|
55f66e6bcd | ||
|
|
22e2b0a6a9 | ||
|
|
e24f3828cb | ||
|
|
70f5bdc499 | ||
|
|
8731a39c3c | ||
|
|
e2d9e75475 | ||
|
|
55c7707b94 | ||
|
|
98944d4815 | ||
|
|
faba25acbd | ||
|
|
aab5ee344c | ||
|
|
6880f88dbe | ||
|
|
9177fe86dc | ||
|
|
bf3f8acbc0 | ||
|
|
b4fa9826f2 | ||
|
|
955de3133c | ||
|
|
faac3d0f57 | ||
|
|
3fb1ede770 | ||
|
|
53224886e3 | ||
|
|
e513180d7e | ||
|
|
3116d6df1a | ||
|
|
4284a4d16d | ||
|
|
eadca3b2c0 | ||
|
|
68125af29f | ||
|
|
d436e628fe | ||
|
|
c5483c81de | ||
|
|
9db4a04cd9 | ||
|
|
8943e8c2f6 | ||
|
|
f17fa55426 | ||
|
|
9ff83075d6 | ||
|
|
6b6043afdc | ||
|
|
fd9162e70f | ||
|
|
8331059b7c | ||
|
|
f0992b4914 | ||
|
|
e081a4cfc2 | ||
|
|
895b3ca7c9 | ||
|
|
4d30264950 | ||
|
|
3a352a94f6 | ||
|
|
8623175612 | ||
|
|
ea531777c4 | ||
|
|
2639ef7e13 | ||
|
|
0e76a19c96 | ||
|
|
3ec9eb377d | ||
|
|
44b7000509 | ||
|
|
251307f1e6 | ||
|
|
75e638ec4d | ||
|
|
443ccdd6c0 | ||
|
|
dbf11cc04b | ||
|
|
a5c9729f5f | ||
|
|
a6e2983a22 | ||
|
|
a21f0c46c7 | ||
|
|
ed4a81e852 | ||
|
|
9231218350 | ||
|
|
7d1ea4ee2b | ||
|
|
0c68e51930 | ||
|
|
8e565dc610 | ||
|
|
5b47844fa1 | ||
|
|
0d01d3246a | ||
|
|
bb31c4ba14 | ||
|
|
d9a7952467 | ||
|
|
208dcbbda3 | ||
|
|
ff4494d395 | ||
|
|
db4b32119e | ||
|
|
bfef5b1877 | ||
|
|
91962cc90b | ||
|
|
7a71a5ddd4 | ||
|
|
ca47a589ba | ||
|
|
a33dbd8922 | ||
|
|
aaab4ca741 | ||
|
|
6aadf7429d | ||
|
|
748307bc91 | ||
|
|
559389a319 | ||
|
|
a9140b6a76 | ||
|
|
8f50e873c9 | ||
|
|
9b28b7f78a | ||
|
|
4417097f97 | ||
|
|
93fc036d2c | ||
|
|
7f13fc58b9 | ||
|
|
527d1f6c93 | ||
|
|
0ec3c496d8 | ||
|
|
62ec3de0b1 | ||
|
|
2c47cb6a3b | ||
|
|
dc2b145843 | ||
|
|
c958e1578a | ||
|
|
1e857e5c0a | ||
|
|
9bc8d48415 | ||
|
|
f86c6734a7 | ||
|
|
3d5ff82062 | ||
|
|
8b63483e2e | ||
|
|
c97d4288c0 | ||
|
|
ff75364f22 | ||
|
|
7f1b028099 | ||
|
|
9e70a985e4 | ||
|
|
057cf273dd | ||
|
|
9af621294d | ||
|
|
bfb5a31032 | ||
|
|
6308955e89 | ||
|
|
b0c5b629d7 | ||
|
|
10f965570b | ||
|
|
c24f5200f5 | ||
|
|
f922692a1a | ||
|
|
65c137f67d | ||
|
|
585679969a | ||
|
|
893b67b6c4 | ||
|
|
3c296b440e | ||
|
|
f8a59b52f7 | ||
|
|
17d106cfda | ||
|
|
dcf9d30382 | ||
|
|
eafd7f6b77 | ||
|
|
f31165b442 | ||
|
|
40c5d5871e | ||
|
|
2362754356 | ||
|
|
8ccf3b589e | ||
|
|
2dbc73c75b | ||
|
|
87e1ad7115 | ||
|
|
2e9895aceb | ||
|
|
fbbc573f33 | ||
|
|
8bc8a045b6 | ||
|
|
8c049b29b3 | ||
|
|
f1e783f047 | ||
|
|
3baa6c7145 | ||
|
|
dd28bb46db | ||
|
|
c182091fa9 | ||
|
|
d061572ed6 | ||
|
|
3a943fbc81 | ||
|
|
8a58a51a2c | ||
|
|
e92265ceaa | ||
|
|
b7b1fff1c0 | ||
|
|
886f7021f4 | ||
|
|
2b17188087 | ||
|
|
d498988bd4 | ||
|
|
785a08ba2c | ||
|
|
6aa83102e4 | ||
|
|
97fe41f3ba | ||
|
|
b7cecfbb66 | ||
|
|
7f7d80e653 | ||
|
|
64796a96fc | ||
|
|
83cc7dc86e | ||
|
|
75c9145558 | ||
|
|
869f459d98 | ||
|
|
e00008b663 | ||
|
|
175f2482e1 | ||
|
|
9d91d72a72 | ||
|
|
fd9cd1b98b | ||
|
|
2640040359 | ||
|
|
dec8f8e896 | ||
|
|
b4eff25636 | ||
|
|
2a752e5358 | ||
|
|
b2684837c1 | ||
|
|
2fed2133a5 | ||
|
|
7fc0877c26 | ||
|
|
f7ce774ddc | ||
|
|
174c730f1f | ||
|
|
8ad513e4ec | ||
|
|
a0bf5b4067 | ||
|
|
484f603c9e | ||
|
|
94b16fb327 | ||
|
|
55db7bdf76 | ||
|
|
14af241d5f | ||
|
|
4efa90e42f | ||
|
|
581e1759c4 | ||
|
|
61cdac321e | ||
|
|
59a04137a1 | ||
|
|
eba1a1ffff | ||
|
|
df25b179d7 | ||
|
|
ff2d9ad67d | ||
|
|
485b72cd0d | ||
|
|
57eb5a4328 |
@@ -106,12 +106,6 @@ LARK_OPEN_APP_ID=""
|
||||
LARK_OPEN_APP_SECRET=""
|
||||
LARK_OPEN_VERIFICATION_TOKEN=""
|
||||
|
||||
# - WEB3
|
||||
# Used for the Web3 plugin
|
||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/rainbow/README.md
|
||||
ALCHEMY_API_KEY=""
|
||||
INFURA_API_KEY=""
|
||||
|
||||
# - SALESFORCE
|
||||
# Used for the Salesforce (Sales Cloud) app
|
||||
SALESFORCE_CONSUMER_KEY=""
|
||||
|
||||
+38
-1
@@ -5,6 +5,7 @@
|
||||
# - SHARED
|
||||
# - NEXTAUTH
|
||||
# - E-MAIL SETTINGS
|
||||
# - ORGANIZATIONS
|
||||
|
||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||
@@ -22,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.
|
||||
@@ -32,6 +35,8 @@ PRISMA_GENERATE_DATAPROXY=
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
# Set this to http://app.cal.local:3000 if you want to enable organizations, and
|
||||
# check variable ORGANIZATIONS_ENABLED at the bottom of this file
|
||||
NEXT_PUBLIC_WEBAPP_URL='http://localhost:3000'
|
||||
# Change to 'http://localhost:3001' if running the website simultaneously
|
||||
NEXT_PUBLIC_WEBSITE_URL='http://localhost:3000'
|
||||
@@ -54,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
|
||||
@@ -71,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
|
||||
@@ -121,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=
|
||||
@@ -167,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"
|
||||
@@ -183,3 +197,26 @@ CSP_POLICY=
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
|
||||
# - ORGANIZATIONS *******************************************************************************************
|
||||
# Enable Organizations non-prod domain setup, works in combination with organizations feature flag
|
||||
# This is mainly needed locally, because for orgs to work a full domain name needs to point
|
||||
# to the app, i.e. app.cal.local instead of using localhost, which is very disruptive
|
||||
#
|
||||
# This variable should only be set to 1 or true if you are in a non-prod environment and you want to
|
||||
# use organizations
|
||||
ORGANIZATIONS_ENABLED=
|
||||
|
||||
# Vercel Config to create subdomains for organizations
|
||||
# Get it from https://vercel.com/<TEAM_OR_USER_NAME>/<PROJECT_SLUG>/settings
|
||||
PROJECT_ID_VERCEL=
|
||||
# Get it from: https://vercel.com/teams/<TEAM_SLUG>/settings
|
||||
TEAM_ID_VERCEL=
|
||||
# Get it from: https://vercel.com/account/tokens
|
||||
AUTH_BEARER_TOKEN_VERCEL=
|
||||
|
||||
#Enables New booker for Embed only
|
||||
NEW_BOOKER_ENABLED_FOR_EMBED=0
|
||||
|
||||
#Enables New booker for All but Embed requests
|
||||
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0
|
||||
|
||||
@@ -8,8 +8,6 @@ Fixes # (issue)
|
||||
Loom Video: https://www.loom.com/
|
||||
-->
|
||||
|
||||
**Environment**: Staging(main branch) / Production
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Please delete bullets that are not relevant. -->
|
||||
@@ -27,13 +25,16 @@ Fixes # (issue)
|
||||
- [ ] Test A
|
||||
- [ ] 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 performed a self-review of my own code and corrected any misspellings
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my PR needs changes to the documentation
|
||||
- I haven't checked if my changes generate no new warnings
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -40,6 +40,14 @@ runs:
|
||||
restore-keys: |
|
||||
yarn-download-cache-
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore node_modules
|
||||
id: yarn-nm-cache
|
||||
uses: buildjet/cache@v3
|
||||
with:
|
||||
path: "**/node_modules/"
|
||||
key: ${{ runner.os }}-yarn-nm-cache-${{ hashFiles('yarn.lock', '.yarnrc.yml') }}
|
||||
|
||||
# Invalidated on yarn.lock changes
|
||||
- name: Restore yarn install state
|
||||
id: yarn-install-state-cache
|
||||
@@ -58,5 +66,6 @@ runs:
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS: "false" # So it doesn't try to remove our private submodule deps
|
||||
YARN_ENABLE_GLOBAL_CACHE: "false" # Use local cache folder to keep downloaded archives
|
||||
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz # Very small speedup when lock does not change
|
||||
YARN_NM_MODE: "hardlinks-local" # Reduce node_modules size
|
||||
# Other environment variables
|
||||
HUSKY: "0" # By default do not run HUSKY install
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
name: Add PRs to project Reviewing PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-PR-to-project:
|
||||
name: Add PRs to project Reviewing PRs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.1.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/calcom/projects/11
|
||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
@@ -1,27 +1,75 @@
|
||||
name: "Apply issue labels to PR"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
permissions:
|
||||
contents: none
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Apply labels from linked issue to PR
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
||||
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
async function getLinkedIssues(owner, repo, prNumber) {
|
||||
const query = `query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const variables = {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
prNumber: prNumber,
|
||||
};
|
||||
|
||||
const result = await github.graphql(query, variables);
|
||||
return result.repository.pullRequest.closingIssuesReferences.nodes;
|
||||
}
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const linkedIssues = await getLinkedIssues(
|
||||
context.repo.owner,
|
||||
context.repo.repo,
|
||||
pr.number
|
||||
);
|
||||
|
||||
const labelsToAdd = new Set();
|
||||
for (const issue of linkedIssues) {
|
||||
if (issue.labels && issue.labels.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
labelsToAdd.add(label.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (labelsToAdd.size) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: Array.from(labelsToAdd),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -25,7 +25,9 @@ jobs:
|
||||
- uses: ./.github/actions/cache-db
|
||||
- uses: ./.github/actions/cache-build
|
||||
- name: Run Tests
|
||||
run: yarn test-e2e:embed-react
|
||||
run: |
|
||||
yarn test-e2e:embed-react
|
||||
yarn workspace @calcom/embed-react packaged:tests
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
- name: Upload Test Results
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- name: Save Code Linting Reports
|
||||
- name: Run Linting with Reports
|
||||
run: yarn lint:report
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
permissions:
|
||||
pull-requests: read
|
||||
@@ -33,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
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
const https = require("https");
|
||||
|
||||
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
||||
// Get the labels from issues linked to the PR
|
||||
const query = `
|
||||
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
closingIssuesReferences(first: 10) {
|
||||
nodes {
|
||||
number
|
||||
labels(first: 10) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const graphqlData = JSON.stringify({
|
||||
query,
|
||||
variables: {
|
||||
owner: pr.base.repo.owner.login,
|
||||
repo: pr.base.repo.name,
|
||||
prNumber: pr.number,
|
||||
},
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
hostname: "api.github.com",
|
||||
path: "/graphql",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": graphqlData.length,
|
||||
Authorization: "Bearer " + token,
|
||||
"User-Agent": "Node.js",
|
||||
},
|
||||
};
|
||||
|
||||
// Use the native Node.js request library to make the request
|
||||
let linkedIssues;
|
||||
linkedIssues = await new Promise((resolve, reject) => {
|
||||
const request = https.request(requestOptions, (response) => {
|
||||
let responseBody = "";
|
||||
response.on("data", (chunk) => {
|
||||
responseBody += chunk;
|
||||
});
|
||||
response.on("end", () => {
|
||||
resolve(JSON.parse(responseBody)?.data?.repository?.pullRequest?.closingIssuesReferences?.nodes);
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
request.write(graphqlData);
|
||||
request.end();
|
||||
});
|
||||
|
||||
if (!linkedIssues || linkedIssues.length === 0) {
|
||||
console.log("No issue linked.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate over linked issues and apply labels to PR
|
||||
for (const issue of linkedIssues) {
|
||||
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
||||
|
||||
if (!labels || labels.length === 0) {
|
||||
console.log(`No labels found on the linked issue #${issue.number}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const labelsData = JSON.stringify({
|
||||
labels: labels,
|
||||
});
|
||||
|
||||
const requestOptions = {
|
||||
hostname: "api.github.com",
|
||||
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": labelsData.length,
|
||||
Authorization: "Bearer " + token,
|
||||
"User-Agent": "Node.js",
|
||||
},
|
||||
};
|
||||
|
||||
let labelResult;
|
||||
labelResult = await new Promise((resolve, reject) => {
|
||||
const request = https.request(requestOptions, (response) => {
|
||||
let responseBody = "";
|
||||
response.on("data", (chunk) => {
|
||||
responseBody += chunk;
|
||||
});
|
||||
response.on("data", () => {});
|
||||
response.on("end", () => {
|
||||
resolve(JSON.parse(responseBody));
|
||||
});
|
||||
});
|
||||
|
||||
request.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
request.write(labelsData);
|
||||
request.end();
|
||||
});
|
||||
|
||||
if (labelResult.message) {
|
||||
console.log(`Error labelling PR: ${labelResult.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Applied labels: ${labels.join(", ")} to PR #${pr.number} from linked issue #${issue.number}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the PR data and GitHub token to the script
|
||||
(async () => {
|
||||
if (!process.env.PR_DATA) {
|
||||
console.log("No PR data found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const prData = JSON.parse(process.env.PR_DATA);
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
await applyLabelFromLinkedIssueToPR(prData, token);
|
||||
})();
|
||||
@@ -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 [slack](https://calendso.slack.com/)
|
||||
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.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
## About the Project
|
||||
|
||||
<img width="100%" alt="booking-screen" src="https://user-images.githubusercontent.com/8019099/176390354-f1bc7069-0341-437a-9bb8-eb41092b4016.gif">
|
||||
<img width="100%" alt="booking-screen" src="https://github.com/calcom/cal.com/assets/8019099/407e727e-ff19-4ca4-bcae-049dca05cf02">
|
||||
|
||||
# Scheduling infrastructure for absolutely everyone
|
||||
|
||||
@@ -90,9 +90,16 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
||||
- [Prisma.io](https://prisma.io/?ref=cal.com)
|
||||
- [Daily.co](https://go.cal.com/daily)
|
||||
|
||||
|
||||
## Contact us
|
||||
|
||||
Meet our sales team for any commercial inquiries.
|
||||
|
||||
<a href="https://cal.com/sales"><img src="https://cal.com/book-with-cal-dark.svg" alt="Book us with Cal.com"></a>
|
||||
|
||||
## Stay Up-to-Date
|
||||
|
||||
Cal.com officially launched as v.1.0 on 15th of September, however a lot of new features are coming. Watch **releases** of this repository to be notified for future updates:
|
||||
Cal.com officially launched as v.1.0 on 15th of September 2021 and we've come a long way so far. Watch **releases** of this repository to be notified of future updates:
|
||||
|
||||

|
||||
|
||||
@@ -121,9 +128,9 @@ Here is what you need to be able to run Cal.com.
|
||||
```sh
|
||||
git clone https://github.com/calcom/cal.com.git
|
||||
```
|
||||
> If you are on windows, run the following command on `gitbash` with admin privileges: <br>
|
||||
```git clone -c core.symlinks=true https://github.com/calcom/cal.com.git``` <br>
|
||||
See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
||||
|
||||
> If you are on windows, run the following command on `gitbash` with admin privileges: <br> > `git clone -c core.symlinks=true https://github.com/calcom/cal.com.git` <br>
|
||||
> See [docs](https://cal.com/docs/how-to-guides/how-to-troubleshoot-symbolic-link-issues-on-windows#enable-symbolic-links) for more details.
|
||||
|
||||
1. Go to the project folder
|
||||
|
||||
@@ -158,6 +165,7 @@ yarn dx
|
||||
```sh
|
||||
echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
```
|
||||
|
||||
#### Gitpod Setup
|
||||
|
||||
1. Click the button below to open this project in Gitpod.
|
||||
@@ -166,8 +174,6 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
|
||||
[](https://gitpod.io/#https://github.com/calcom/cal.com)
|
||||
|
||||
|
||||
|
||||
#### Manual setup
|
||||
|
||||
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, and `<db-port>` with their applicable values
|
||||
@@ -175,24 +181,26 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
```
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>If you don't know how to configure the DATABASE_URL, then follow the steps here to create a quick local DB</summary>
|
||||
|
||||
1. [Download](https://www.postgresql.org/download/) and install postgres in your local (if you don't have it already).
|
||||
|
||||
2. Create your own local db by executing `createDB <DB name>`
|
||||
2. Create your own local db by executing `createDB <DB name>`
|
||||
|
||||
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
|
||||
|
||||
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||

|
||||
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
`postgresql://postgres:postgres@localhost:5432/Your-DB-Name`.
|
||||
|
||||
</details>
|
||||
|
||||
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
|
||||
|
||||
- [Setup postgres DB with railway.app](https://arctype.com/postgres/setup/railway-postgres)
|
||||
- [Setup postgres DB with render](https://render.com/docs/databases)
|
||||
|
||||
@@ -201,9 +209,18 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
1. Set a 32 character random string in your `.env` file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||
|
||||
In a development environment, run:
|
||||
|
||||
```sh
|
||||
yarn workspace @calcom/prisma db-migrate
|
||||
```
|
||||
|
||||
In a production environment, run:
|
||||
|
||||
```sh
|
||||
yarn workspace @calcom/prisma db-deploy
|
||||
```
|
||||
|
||||
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||
|
||||
```sh
|
||||
@@ -308,12 +325,6 @@ Issues with Docker? Find your answer or open a new discussion [here](https://git
|
||||
|
||||
Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
||||
|
||||
### Heroku
|
||||
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/calcom/cal.com">
|
||||
<img width="185px" height="auto" src="https://herokucdn.com/deploy/button.svg" alt="Deploy">
|
||||
</a>
|
||||
|
||||
### Railway
|
||||
|
||||
[](https://railway.app/new/template/cal)
|
||||
@@ -416,7 +427,7 @@ Make sure to complete section "Obtaining the Google API Credentials". After the
|
||||
following
|
||||
|
||||
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
||||
1. Under 'OAuth concent screen', click "PUBLISH APP"
|
||||
1. Under 'OAuth consent screen', click "PUBLISH APP"
|
||||
|
||||
### Obtaining Microsoft Graph Client ID and Secret
|
||||
|
||||
@@ -482,7 +493,9 @@ following
|
||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||
|
||||
### Obtaining Zoho Bigin Client ID and Secret
|
||||
|
||||
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||
|
||||
## Workflows
|
||||
|
||||
### Setting up SendGrid for Email reminders
|
||||
|
||||
+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.
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
@@ -119,8 +120,10 @@ const availabilitySchema = z
|
||||
async function handler(req: NextApiRequest) {
|
||||
const { prisma, isAdmin, userId: reqUserId } = req;
|
||||
const { username, userId, eventTypeId, dateTo, dateFrom, teamId } = availabilitySchema.parse(req.query);
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(req.headers.host ?? "");
|
||||
if (!teamId)
|
||||
return getUserAvailability({
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain || undefined : undefined,
|
||||
username,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -33,7 +33,7 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que
|
||||
* type: integer
|
||||
* example: [2, 3, 4]
|
||||
* - in: query
|
||||
* name: attendeeEmails
|
||||
* name: attendeeEmail
|
||||
* required: false
|
||||
* schema:
|
||||
* oneOf:
|
||||
|
||||
@@ -26,14 +26,10 @@ import { defaultResponder } from "@calcom/lib/server";
|
||||
* required:
|
||||
* - eventTypeId
|
||||
* - start
|
||||
* - end
|
||||
* - name
|
||||
* - email
|
||||
* - responses
|
||||
* - timeZone
|
||||
* - language
|
||||
* - metadata
|
||||
* - customInputs
|
||||
* - location
|
||||
* properties:
|
||||
* eventTypeId:
|
||||
* type: integer
|
||||
@@ -46,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'
|
||||
@@ -97,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",
|
||||
@@ -118,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;
|
||||
}) {
|
||||
|
||||
@@ -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,8 +48,8 @@
|
||||
"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": "^2.9.15"
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const SnippedGenerator = {
|
||||
// extend some internals to gain information about current path, method and spec in the generator function metioned later
|
||||
spec: {
|
||||
wrapSelectors: {
|
||||
requestFor: (ori, system) => (state, path, method) => {
|
||||
requestFor: (ori) => (state, path, method) => {
|
||||
return ori(path, method)
|
||||
?.set("spec", state.get("json", {}))
|
||||
?.setIn(["oasPathMethod", "path"], path)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
+131
-28
@@ -8,12 +8,16 @@ import { createRef, forwardRef, useRef, useState } from "react";
|
||||
import type { ControlProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import { APP_NAME, EMBED_LIB_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { BookerLayout } from "@calcom/features/bookings/Booker/types";
|
||||
import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
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,
|
||||
@@ -35,18 +39,27 @@ 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>;
|
||||
floatingPopup: {
|
||||
config?: {
|
||||
layout: BookerLayouts;
|
||||
};
|
||||
[key: string]: string | boolean | undefined | Record<string, string>;
|
||||
};
|
||||
elementClick: Record<string, string>;
|
||||
palette: {
|
||||
brandColor: string;
|
||||
};
|
||||
hideEventTypeDetails: boolean;
|
||||
layout: BookerLayouts;
|
||||
};
|
||||
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
|
||||
|
||||
@@ -129,11 +142,13 @@ const getEmbedUIInstructionString = ({
|
||||
theme,
|
||||
brandColor,
|
||||
hideEventTypeDetails,
|
||||
layout,
|
||||
}: {
|
||||
apiName: string;
|
||||
theme?: string;
|
||||
brandColor: string;
|
||||
hideEventTypeDetails: boolean;
|
||||
layout?: string;
|
||||
}) => {
|
||||
theme = theme !== "auto" ? theme : undefined;
|
||||
return getInstructionString({
|
||||
@@ -147,6 +162,7 @@ const getEmbedUIInstructionString = ({
|
||||
},
|
||||
},
|
||||
hideEventTypeDetails: hideEventTypeDetails,
|
||||
layout,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -168,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": ({
|
||||
@@ -186,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}`;
|
||||
@@ -233,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}`;
|
||||
},
|
||||
},
|
||||
@@ -260,6 +322,7 @@ const getEmbedTypeSpecificString = ({
|
||||
theme: PreviewState["theme"];
|
||||
brandColor: string;
|
||||
hideEventTypeDetails: boolean;
|
||||
layout?: BookerLayout;
|
||||
};
|
||||
if (embedFramework === "react") {
|
||||
uiInstructionStringArg = {
|
||||
@@ -267,6 +330,7 @@ const getEmbedTypeSpecificString = ({
|
||||
theme: previewState.theme,
|
||||
brandColor: previewState.palette.brandColor,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
layout: previewState.layout,
|
||||
};
|
||||
} else {
|
||||
uiInstructionStringArg = {
|
||||
@@ -274,6 +338,7 @@ const getEmbedTypeSpecificString = ({
|
||||
theme: previewState.theme,
|
||||
brandColor: previewState.palette.brandColor,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
layout: previewState.layout,
|
||||
};
|
||||
}
|
||||
if (!frameworkCodes[embedType]) {
|
||||
@@ -288,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]({
|
||||
@@ -626,6 +692,7 @@ const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme;
|
||||
const ChooseEmbedTypesDialogContent = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||
<div className="mb-2">
|
||||
@@ -671,6 +738,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
const router = useRouter();
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const dialogContentRef = useRef<HTMLDivElement>(null);
|
||||
const flags = useFlagMap();
|
||||
const isBookerLayoutsEnabled = flags["booker-layouts"] === true;
|
||||
|
||||
const s = (href: string) => {
|
||||
const searchParams = new URLSearchParams(router.asPath.split("?")[1] || "");
|
||||
@@ -691,12 +760,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
|
||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||
const [previewState, setPreviewState] = useState({
|
||||
const [previewState, setPreviewState] = useState<PreviewState>({
|
||||
inline: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
theme: Theme.auto,
|
||||
layout: BookerLayouts.MONTH_VIEW,
|
||||
floatingPopup: {},
|
||||
elementClick: {},
|
||||
hideEventTypeDetails: false,
|
||||
@@ -764,6 +834,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
name: "ui",
|
||||
arg: {
|
||||
theme: previewState.theme,
|
||||
layout: previewState.layout,
|
||||
hideEventTypeDetails: previewState.hideEventTypeDetails,
|
||||
styles: {
|
||||
branding: {
|
||||
@@ -798,6 +869,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
{ value: Theme.light, label: "Light Theme" },
|
||||
];
|
||||
|
||||
const layoutOptions = [
|
||||
{ value: BookerLayouts.MONTH_VIEW, label: t("bookerlayout_month_view") },
|
||||
{ value: BookerLayouts.WEEK_VIEW, label: t("bookerlayout_week_view") },
|
||||
{ value: BookerLayouts.COLUMN_VIEW, label: t("bookerlayout_column_view") },
|
||||
];
|
||||
|
||||
const FloatingPopupPositionOptions = [
|
||||
{
|
||||
value: "bottom-right",
|
||||
@@ -979,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"
|
||||
@@ -1070,6 +1147,34 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
{isBookerLayoutsEnabled && (
|
||||
<Label className="mb-6">
|
||||
<div className="mb-2">{t("layout")}</div>
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={layoutOptions[0]}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
setPreviewState((previewState) => {
|
||||
const config = {
|
||||
...(previewState.floatingPopup.config ?? {}),
|
||||
layout: option.value,
|
||||
};
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
config,
|
||||
},
|
||||
layout: option.value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={layoutOptions}
|
||||
/>
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
@@ -1102,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"
|
||||
@@ -1118,8 +1227,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
{t("copy_code")}
|
||||
</Button>
|
||||
) : null}
|
||||
<DialogClose />
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1133,12 +1241,7 @@ export const EmbedDialog = () => {
|
||||
const router = useRouter();
|
||||
const embedUrl: string = router.query.embedUrl as string;
|
||||
return (
|
||||
<Dialog
|
||||
name="embed"
|
||||
clearQueryParamsOnClose={queryParamsForDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) window.resetEmbedStatus();
|
||||
}}>
|
||||
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
||||
{!router.query.embedType ? (
|
||||
<ChooseEmbedTypesDialogContent />
|
||||
) : (
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { Dispatch } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import type { JSONObject } from "superjson/dist/types";
|
||||
|
||||
export type Gate = undefined | "rainbow"; // Add more like ` | "geolocation" | "payment"`
|
||||
|
||||
export type GateState = {
|
||||
rainbowToken?: string;
|
||||
};
|
||||
|
||||
type GateProps = {
|
||||
children: React.ReactNode;
|
||||
gates: Gate[];
|
||||
appData: JSONObject;
|
||||
dispatch: Dispatch<Partial<GateState>>;
|
||||
};
|
||||
|
||||
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
|
||||
|
||||
// To add a new Gate just add the gate logic to the switch statement
|
||||
const Gates: React.FC<GateProps> = ({ children, gates, appData, dispatch }) => {
|
||||
const [rainbowToken, setRainbowToken] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ rainbowToken });
|
||||
}, [rainbowToken, dispatch]);
|
||||
|
||||
let gateWrappers = <>{children}</>;
|
||||
|
||||
// Recursively wraps the `gateWrappers` with new gates allowing for multiple gates
|
||||
for (const gate of gates) {
|
||||
switch (gate) {
|
||||
case "rainbow":
|
||||
if (appData.blockchainId && appData.smartContractAddress && !rainbowToken) {
|
||||
gateWrappers = (
|
||||
<RainbowGate
|
||||
setToken={setRainbowToken}
|
||||
chainId={appData.blockchainId as number}
|
||||
tokenAddress={appData.smartContractAddress as string}>
|
||||
{gateWrappers}
|
||||
</RainbowGate>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gateWrappers;
|
||||
};
|
||||
|
||||
export default Gates;
|
||||
@@ -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}>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { DefaultSeo } from "next-seo";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
|
||||
import "@calcom/embed-core/src/embed-iframe";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { buildCanonical } from "@calcom/lib/next-seo.config";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import AppProviders from "@lib/app-providers";
|
||||
@@ -13,7 +16,7 @@ import { seoConfig } from "@lib/config/next-seo.config";
|
||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||
|
||||
export interface CalPageWrapper {
|
||||
(props?: any): JSX.Element;
|
||||
(props?: AppProps): JSX.Element;
|
||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
}
|
||||
|
||||
@@ -49,9 +52,30 @@ function PageWrapper(props: AppProps) {
|
||||
};
|
||||
// Use the layout defined at the page level, if available
|
||||
const getLayout = Component.getLayout ?? ((page) => page);
|
||||
|
||||
// Canonical: Check if the URL is from cal.com so that we set the canonical conditionally
|
||||
const isCalcom =
|
||||
WEBAPP_URL &&
|
||||
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") || new URL(WEBAPP_URL).hostname.endsWith("cal.dev"));
|
||||
const path = router.asPath;
|
||||
|
||||
return (
|
||||
<AppProviders {...providerProps}>
|
||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
</Head>
|
||||
<DefaultSeo
|
||||
// Set canonical to https://cal.com or self-hosted URL
|
||||
canonical={
|
||||
isCalcom
|
||||
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev
|
||||
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
|
||||
}
|
||||
{...seoConfig.defaultNextSeo}
|
||||
/>
|
||||
<I18nLanguageHandler />
|
||||
<Script
|
||||
nonce={nonce}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FunctionComponent, SVGProps } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -55,7 +57,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
<InstallAppButton
|
||||
type={data.type}
|
||||
render={(installProps) => {
|
||||
const props = { ...installProps } as any;
|
||||
const props = { ...installProps } as FunctionComponent<SVGProps<SVGSVGElement>>;
|
||||
return (
|
||||
<DropdownItem {...props} color="minimal" type="button">
|
||||
<span className="flex items-center gap-x-2">
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
|
||||
@@ -2,60 +2,12 @@ import type { ComponentProps } from "react";
|
||||
import React from "react";
|
||||
|
||||
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
|
||||
import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
||||
|
||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||
{
|
||||
name: "calendar",
|
||||
href: "/apps/installed/calendar",
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
name: "conferencing",
|
||||
href: "/apps/installed/conferencing",
|
||||
icon: Video,
|
||||
},
|
||||
{
|
||||
name: "payment",
|
||||
href: "/apps/installed/payment",
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "automation",
|
||||
href: "/apps/installed/automation",
|
||||
icon: Share2,
|
||||
},
|
||||
{
|
||||
name: "analytics",
|
||||
href: "/apps/installed/analytics",
|
||||
icon: BarChart,
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
href: "/apps/installed/other",
|
||||
icon: Grid,
|
||||
},
|
||||
];
|
||||
|
||||
export default function InstalledAppsLayout({
|
||||
children,
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const variant: (typeof InstalledAppVariants)[number] = "payment";
|
||||
|
||||
const query = trpc.viewer.integrations.useQuery({
|
||||
variant,
|
||||
onlyInstalled: true,
|
||||
});
|
||||
let actualTabs = tabs;
|
||||
if (query.data?.items.length === 0) {
|
||||
actualTabs = tabs.filter((tab) => tab.name !== variant);
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell {...rest} hideHeadingOnMobile>
|
||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||
|
||||
@@ -26,24 +26,24 @@ export default function TwoFactor({ center = true }) {
|
||||
|
||||
return (
|
||||
<div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}>
|
||||
<Label className="mt-4"> {t("2fa_code")}</Label>
|
||||
<Label className="mt-4">{t("2fa_code")}</Label>
|
||||
|
||||
<p className="text-subtle mb-4 text-sm">{t("2fa_enabled_instructions")}</p>
|
||||
<input hidden type="hidden" value={value} {...methods.register("totpCode")} />
|
||||
|
||||
<input type="hidden" value={value} {...methods.register("totpCode")} />
|
||||
|
||||
<div className="flex flex-row justify-between">
|
||||
<Input
|
||||
className={className}
|
||||
name="2fa1"
|
||||
inputMode="decimal"
|
||||
{...digits[0]}
|
||||
autoFocus
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
<Input className={className} name="2fa2" inputMode="decimal" {...digits[1]} />
|
||||
<Input className={className} name="2fa3" inputMode="decimal" {...digits[2]} />
|
||||
<Input className={className} name="2fa4" inputMode="decimal" {...digits[3]} />
|
||||
<Input className={className} name="2fa5" inputMode="decimal" {...digits[4]} />
|
||||
<Input className={className} name="2fa6" inputMode="decimal" {...digits[5]} />
|
||||
{digits.map((digit, index) => (
|
||||
<Input
|
||||
key={`2fa${index}`}
|
||||
className={className}
|
||||
name={`2fa${index + 1}`}
|
||||
inputMode="decimal"
|
||||
{...digit}
|
||||
autoFocus={index === 0}
|
||||
autoComplete="one-time-code"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 = [
|
||||
"attendee_in_person",
|
||||
"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">
|
||||
|
||||
@@ -28,7 +28,6 @@ type AvailableTimesProps = {
|
||||
bookingAttendees?: number | null;
|
||||
slots?: Slot[];
|
||||
isLoading: boolean;
|
||||
ethSignature?: string;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
@@ -43,7 +42,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
onTimeFormatChange,
|
||||
seatsPerTimeSlot,
|
||||
bookingAttendees,
|
||||
ethSignature,
|
||||
duration,
|
||||
}) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
@@ -119,7 +117,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
timeFormat,
|
||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||
...(ethSignature ? { ethSignature } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -164,7 +161,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
onClick={() => reserveSlot(slot)}
|
||||
data-testid="time">
|
||||
data-testid="time"
|
||||
data-disabled="false">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
|
||||
@@ -83,7 +83,9 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
size="sm"
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="text-default mt-1 mb-2 break-words text-sm font-medium ">{profile.name}</h2>
|
||||
<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">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -59,11 +59,12 @@ export default function CancelBooking(props: Props) {
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-default font-medium">{t("cancellation_reason")}</label>
|
||||
<TextArea
|
||||
data-testid="cancel_reason"
|
||||
ref={cancelBookingRef}
|
||||
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 ">
|
||||
@@ -75,7 +76,7 @@ export default function CancelBooking(props: Props) {
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
data-testid="confirm_cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -92,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" />
|
||||
|
||||
@@ -80,7 +80,6 @@ export const SlotPicker = ({
|
||||
seatsPerTimeSlot,
|
||||
bookingAttendees,
|
||||
weekStart = 0,
|
||||
ethSignature,
|
||||
}: {
|
||||
eventType: Pick<
|
||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||
@@ -94,7 +93,6 @@ export const SlotPicker = ({
|
||||
recurringEventCount?: number;
|
||||
users: string[];
|
||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
ethSignature?: string;
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||
@@ -193,7 +191,6 @@ export const SlotPicker = ({
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
bookingAttendees={bookingAttendees}
|
||||
recurringCount={recurringEventCount}
|
||||
ethSignature={ethSignature}
|
||||
duration={parseInt(duration)}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
@@ -27,8 +26,6 @@ import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
|
||||
import type { Gate, GateState } from "@components/Gates";
|
||||
import Gates from "@components/Gates";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
import { SlotPicker } from "@components/booking/SlotPicker";
|
||||
|
||||
@@ -89,14 +86,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
setIs24hClockInLocalStorage(is24Hours);
|
||||
};
|
||||
|
||||
const [gateState, gateDispatcher] = useReducer(
|
||||
(state: GateState, newState: Partial<GateState>) => ({
|
||||
...state,
|
||||
...newState,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||
}, []);
|
||||
@@ -124,20 +113,11 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||
|
||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||
|
||||
// Define conditional gates here
|
||||
const gates = [
|
||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
||||
? ("rainbow" as Gate)
|
||||
: undefined,
|
||||
];
|
||||
|
||||
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
|
||||
{
|
||||
seatReferenceUid: rescheduleUid,
|
||||
@@ -148,7 +128,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
||||
<>
|
||||
<HeadSeo
|
||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||
@@ -211,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>
|
||||
|
||||
@@ -238,7 +218,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", {
|
||||
@@ -296,7 +276,6 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
bookingAttendees={bookingAttendees || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
ethSignature={gateState.rainbowToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,7 +285,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
</main>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</Gates>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
@@ -12,7 +12,6 @@ import { z } from "zod";
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
@@ -51,8 +50,6 @@ import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/
|
||||
import { timeZone } from "@lib/clock";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import type { Gate, GateState } from "@components/Gates";
|
||||
import Gates from "@components/Gates";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
|
||||
import type { BookPageProps } from "../../../pages/[user]/book";
|
||||
@@ -224,13 +221,6 @@ const BookingPage = ({
|
||||
const { data: session } = useSession();
|
||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||
const telemetry = useTelemetry();
|
||||
const [gateState, gateDispatcher] = useReducer(
|
||||
(state: GateState, newState: Partial<GateState>) => ({
|
||||
...state,
|
||||
...newState,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
const { timezone } = useTimePreferences();
|
||||
|
||||
@@ -488,7 +478,6 @@ const BookingPage = ({
|
||||
metadata,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
}));
|
||||
recurringMutation.mutate(recurringBookings);
|
||||
} else {
|
||||
@@ -506,25 +495,15 @@ const BookingPage = ({
|
||||
metadata,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
seatReferenceUid: router.query.seatReferenceUid as string,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
|
||||
// Define conditional gates here
|
||||
const gates = [
|
||||
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
|
||||
rainbowAppData && rainbowAppData.blockchainId && rainbowAppData.smartContractAddress
|
||||
? ("rainbow" as Gate)
|
||||
: undefined,
|
||||
];
|
||||
|
||||
return (
|
||||
<Gates gates={gates} appData={rainbowAppData} dispatch={gateDispatcher}>
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{rescheduleUid
|
||||
@@ -555,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 && (
|
||||
@@ -582,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) => (
|
||||
@@ -598,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, {
|
||||
@@ -686,7 +665,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</Gates>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -57,12 +58,15 @@ const LocationInput = (props: {
|
||||
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||
);
|
||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={eventLocationType.variable}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
||||
return <PhoneInput onChange={onChange} value={value} {...rest} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -88,6 +92,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
useEffect(() => {
|
||||
if (selection) {
|
||||
locationFormMethods.setValue("locationType", selection?.value);
|
||||
if (selection?.address) {
|
||||
locationFormMethods.setValue("locationAddress", selection?.address);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection]);
|
||||
@@ -149,12 +156,25 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
name: "locationType",
|
||||
});
|
||||
|
||||
const selectedAddrValue = useWatch({
|
||||
control: locationFormMethods.control,
|
||||
name: "locationAddress",
|
||||
});
|
||||
|
||||
const eventLocationType = getEventLocationType(selectedLocation);
|
||||
|
||||
const defaultLocation = defaultValues?.find(
|
||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
||||
(location: { type: EventLocationType["type"]; address?: string }) => {
|
||||
if (location.type === LocationType.InPerson) {
|
||||
return location.type === eventLocationType?.type && location.address === selectedAddrValue;
|
||||
} else {
|
||||
return location.type === eventLocationType?.type;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(defaultLocation);
|
||||
|
||||
const LocationOptions = (() => {
|
||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||
if (!eventLocationType.variable) {
|
||||
@@ -240,7 +260,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>
|
||||
@@ -292,8 +312,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,13 +49,13 @@ 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>
|
||||
<TextArea
|
||||
data-testid="reschedule_reason"
|
||||
name={t("reschedule_reason")}
|
||||
name={t("reason_for_reschedule")}
|
||||
value={rescheduleReason}
|
||||
onChange={(e) => setRescheduleReason(e.target.value)}
|
||||
className="mb-5 sm:mb-6"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -37,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();
|
||||
|
||||
@@ -48,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";
|
||||
@@ -65,7 +70,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
@@ -152,13 +157,17 @@ 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>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||
<div>
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} />
|
||||
</div>
|
||||
<hr className="border-subtle" />
|
||||
<FormBuilder
|
||||
title={t("booking_questions_title")}
|
||||
@@ -211,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")}
|
||||
@@ -263,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);
|
||||
|
||||
@@ -104,9 +104,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
</div>
|
||||
</div>
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
<div>
|
||||
<div className="bg-muted rounded-md p-8">
|
||||
{!isLoading && notInstalledApps?.length ? (
|
||||
<h2 className="text-emphasis my-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||
<h2 className="text-emphasis text-lg font-semibold">{t("available_apps")}</h2>
|
||||
) : null}
|
||||
<div className="before:border-0">
|
||||
{notInstalledApps?.map((app) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -115,10 +115,18 @@ export const EventSetupTab = (
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||
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) => ({
|
||||
@@ -136,9 +144,17 @@ export const EventSetupTab = (
|
||||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||
);
|
||||
|
||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
||||
const openLocationModal = (type: EventLocationType["type"], address = "") => {
|
||||
const option = getLocationFromType(type, locationOptions);
|
||||
setSelectedLocation(option);
|
||||
if (option && option.value === LocationType.InPerson) {
|
||||
const inPersonOption = {
|
||||
...option,
|
||||
address,
|
||||
};
|
||||
setSelectedLocation(inPersonOption);
|
||||
} else {
|
||||
setSelectedLocation(option);
|
||||
}
|
||||
setShowLocationModal(true);
|
||||
};
|
||||
|
||||
@@ -276,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
|
||||
@@ -290,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
|
||||
@@ -298,10 +314,14 @@ export const EventSetupTab = (
|
||||
onClick={() => {
|
||||
locationFormMethods.setValue("locationType", location.type);
|
||||
locationFormMethods.unregister("locationLink");
|
||||
locationFormMethods.unregister("locationAddress");
|
||||
if (location.type === LocationType.InPerson) {
|
||||
locationFormMethods.setValue("locationAddress", location.address);
|
||||
} else {
|
||||
locationFormMethods.unregister("locationAddress");
|
||||
}
|
||||
locationFormMethods.unregister("locationPhoneNumber");
|
||||
setEditingLocationType(location.type);
|
||||
openLocationModal(location.type);
|
||||
openLocationModal(location.type, location.address);
|
||||
}}
|
||||
aria-label={t("edit")}
|
||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||
@@ -320,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.
|
||||
@@ -517,7 +537,18 @@ export const EventSetupTab = (
|
||||
defaultValues={formMethods.getValues("locations")}
|
||||
selection={
|
||||
selectedLocation
|
||||
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
|
||||
? selectedLocation.address
|
||||
? {
|
||||
value: selectedLocation.value,
|
||||
label: t(selectedLocation.label),
|
||||
icon: selectedLocation.icon,
|
||||
address: selectedLocation.address,
|
||||
}
|
||||
: {
|
||||
value: selectedLocation.value,
|
||||
label: t(selectedLocation.label),
|
||||
icon: selectedLocation.icon,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
|
||||
@@ -75,8 +75,7 @@ function getNavigation(props: {
|
||||
installedAppsNumber: number;
|
||||
availability: AvailabilityOption | undefined;
|
||||
}) {
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||
props;
|
||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
||||
const duration =
|
||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||
|
||||
@@ -178,6 +177,7 @@ function EventTypeSingleLayout({
|
||||
enabledWorkflowsNumber,
|
||||
availability,
|
||||
});
|
||||
|
||||
navigation.splice(1, 0, {
|
||||
name: "availability",
|
||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||
@@ -185,7 +185,7 @@ function EventTypeSingleLayout({
|
||||
info:
|
||||
isManagedEventType || isChildrenManagedEventType
|
||||
? eventType.schedule === null
|
||||
? "Member's default schedule"
|
||||
? "members_default_schedule"
|
||||
: isChildrenManagedEventType
|
||||
? `${
|
||||
eventType.scheduleName
|
||||
@@ -193,7 +193,7 @@ function EventTypeSingleLayout({
|
||||
: `default_schedule_name`
|
||||
}`
|
||||
: eventType.scheduleName ?? `default_schedule_name`
|
||||
: `default_schedule_name`,
|
||||
: eventType.scheduleName ?? `default_schedule_name`,
|
||||
});
|
||||
// If there is a team put this navigation item within the tabs
|
||||
if (team) {
|
||||
@@ -220,7 +220,18 @@ function EventTypeSingleLayout({
|
||||
});
|
||||
}
|
||||
return navigation;
|
||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
|
||||
}, [
|
||||
t,
|
||||
eventType,
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
enabledWorkflowsNumber,
|
||||
availability,
|
||||
isManagedEventType,
|
||||
isChildrenManagedEventType,
|
||||
team,
|
||||
formMethods,
|
||||
]);
|
||||
|
||||
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||
eventType.slug
|
||||
@@ -236,7 +247,7 @@ function EventTypeSingleLayout({
|
||||
heading={eventType.title}
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
{!eventType.metadata.managedEventConfig && (
|
||||
{!eventType.metadata?.managedEventConfig && (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -358,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"
|
||||
@@ -379,7 +390,7 @@ function EventTypeSingleLayout({
|
||||
<Button
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={formMethods.formState.isSubmitting || isUpdateMutationLoading}
|
||||
loading={isUpdateMutationLoading}
|
||||
data-testid="update-eventtype"
|
||||
form="event-type-form">
|
||||
{t("save")}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,7 @@ export default function RequiresConfirmationController({
|
||||
if (!requiresConfirmation) {
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface IConnectCalendarsProps {
|
||||
|
||||
const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
||||
const { nextStep } = props;
|
||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery();
|
||||
const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
|
||||
const { t } = useLocale();
|
||||
const queryIntegrations = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||
|
||||
|
||||
@@ -8,23 +8,17 @@ import { md } from "@calcom/lib/markdownIt";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { Avatar, Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
type FormData = {
|
||||
bio: string;
|
||||
};
|
||||
interface IUserProfileProps {
|
||||
user: IOnboardingPageProps["user"];
|
||||
}
|
||||
|
||||
const UserProfile = (props: IUserProfileProps) => {
|
||||
const { user } = props;
|
||||
const UserProfile = () => {
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const { t } = useLocale();
|
||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||
const avatarRef = useRef<HTMLInputElement>(null);
|
||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||
defaultValues: { bio: user?.bio || "" },
|
||||
});
|
||||
@@ -76,7 +70,7 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
|
||||
async function updateProfileHandler(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const enteredAvatar = avatarRef.current.value;
|
||||
const enteredAvatar = avatarRef.current?.value;
|
||||
mutation.mutate({
|
||||
avatar: enteredAvatar,
|
||||
});
|
||||
@@ -127,14 +121,16 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
id="avatar-upload"
|
||||
buttonMsg={t("add_profile_photo")}
|
||||
handleAvatarChange={(newAvatar) => {
|
||||
avatarRef.current.value = newAvatar;
|
||||
if (avatarRef.current) {
|
||||
avatarRef.current.value = newAvatar;
|
||||
}
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
nativeInputValueSetter?.call(avatarRef.current, newAvatar);
|
||||
const ev2 = new Event("input", { bubbles: true });
|
||||
avatarRef.current.dispatchEvent(ev2);
|
||||
avatarRef.current?.dispatchEvent(ev2);
|
||||
updateProfileHandler(ev2 as unknown as FormEvent<HTMLFormElement>);
|
||||
setImageSrc(newAvatar);
|
||||
}}
|
||||
|
||||
@@ -13,15 +13,13 @@ import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
interface IUserSettingsProps {
|
||||
user: IOnboardingPageProps["user"];
|
||||
nextStep: () => void;
|
||||
}
|
||||
|
||||
const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { user, nextStep } = props;
|
||||
const { nextStep } = props;
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const { t } = useLocale();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||
const telemetry = useTelemetry();
|
||||
@@ -69,7 +67,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="space-y-6">
|
||||
{/* Username textfield */}
|
||||
<UsernameAvailabilityField user={user} />
|
||||
<UsernameAvailabilityField />
|
||||
|
||||
{/* Full name textfield */}
|
||||
<div className="w-full">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
@@ -23,20 +24,20 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
|
||||
return (
|
||||
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={member.name || ""}
|
||||
imageSrc={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
|
||||
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: md.render(markdownToSafeHTML(member.bio)) }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,14 +8,12 @@ import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/ut
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||
import { Star as StarSolid } from "@calcom/ui/components/icon";
|
||||
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||
import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
|
||||
|
||||
export enum UsernameChangeStatusEnum {
|
||||
UPGRADE = "UPGRADE",
|
||||
@@ -29,7 +27,6 @@ interface ICustomUsernameProps {
|
||||
setInputUsernameValue: (value: string) => void;
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
user: Pick<User, "username" | "metadata">;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
@@ -57,8 +54,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
readonly: disabled,
|
||||
user,
|
||||
} = props;
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||
const [markAsError, setMarkAsError] = useState(false);
|
||||
const router = useRouter();
|
||||
@@ -227,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",
|
||||
|
||||
@@ -21,7 +21,7 @@ interface ICustomUsernameProps {
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
}
|
||||
|
||||
const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.ComponentProps<typeof TextField>>) => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
currentUsername,
|
||||
@@ -31,6 +31,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
usernameRef,
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
...rest
|
||||
} = props;
|
||||
const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
|
||||
const [markAsError, setMarkAsError] = useState(false);
|
||||
@@ -79,7 +80,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
const ActionButtons = () => {
|
||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
||||
<div className="me-2 ms-2 flex flex-row space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpenDialogSaveUsername(true)}
|
||||
@@ -116,9 +117,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
ref={usernameRef}
|
||||
name="username"
|
||||
value={inputUsernameValue}
|
||||
addOnLeading={
|
||||
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
|
||||
}
|
||||
autoComplete="none"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="none"
|
||||
@@ -133,6 +131,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
setInputUsernameValue(event.target.value);
|
||||
}}
|
||||
data-testid="username-input"
|
||||
{...rest}
|
||||
/>
|
||||
{currentUsername !== inputUsernameValue && (
|
||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||
@@ -142,7 +141,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5 hidden md:inline">
|
||||
<div className="mt-7 hidden md:inline">
|
||||
<ActionButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -17,14 +19,24 @@ export const UsernameAvailability = IS_SELF_HOSTED ? UsernameTextfield : Premium
|
||||
interface UsernameAvailabilityFieldProps {
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
user: Pick<User, "username" | "metadata">;
|
||||
}
|
||||
|
||||
function useUserNamePrefix(organization: RouterOutputs["viewer"]["me"]["organization"]): string {
|
||||
return organization
|
||||
? organization.slug
|
||||
? getOrgFullDomain(organization.slug, { protocol: false })
|
||||
: organization.metadata && organization.metadata.requestedSlug
|
||||
? 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://", "");
|
||||
}
|
||||
|
||||
export const UsernameAvailabilityField = ({
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
user,
|
||||
}: UsernameAvailabilityFieldProps) => {
|
||||
const router = useRouter();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
|
||||
const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
|
||||
const { username: currentUsername, setQuery: setCurrentUsername } =
|
||||
@@ -37,6 +49,8 @@ export const UsernameAvailabilityField = ({
|
||||
},
|
||||
});
|
||||
|
||||
const usernamePrefix = useUserNamePrefix(user.organization);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={formMethods.control}
|
||||
@@ -51,7 +65,7 @@ export const UsernameAvailabilityField = ({
|
||||
setInputUsernameValue={onChange}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
user={user}
|
||||
addOnLeading={usernamePrefix}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -26,12 +26,6 @@ export const CheckedSelect = ({
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
styles={{
|
||||
option: (styles, { isDisabled }) => ({
|
||||
...styles,
|
||||
backgroundColor: isDisabled ? "#F5F5F5" : "inherit",
|
||||
}),
|
||||
}}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder || t("select")}
|
||||
isSearchable={false}
|
||||
|
||||
@@ -11,6 +11,7 @@ export type LocationOption = {
|
||||
value: EventLocationType["type"];
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -66,6 +66,7 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
||||
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
||||
return opts.empty(query);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return opts.success(query as any);
|
||||
}
|
||||
|
||||
@@ -88,9 +89,10 @@ const withQuery = <
|
||||
TInput = inferProcedureInput<TQuery>,
|
||||
TOutput = inferProcedureOutput<TQuery>
|
||||
>(
|
||||
queryProcedure: DecorateProcedure<TQuery, any, any>,
|
||||
queryProcedure: DecorateProcedure<TQuery, inferProcedureInput<TQuery>, inferProcedureOutput<TQuery>>,
|
||||
|
||||
input?: TInput,
|
||||
params?: UseTRPCQueryOptions<any, TInput, TOutput, TOutput, TError>
|
||||
params?: UseTRPCQueryOptions<TQuery, TInput, TOutput, TOutput, TError>
|
||||
) => {
|
||||
return function WithQuery(
|
||||
opts: Omit<
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { NextRouter } from "next/router";
|
||||
import { useRouter } from "next/router";
|
||||
import type { ComponentProps, PropsWithChildren, ReactNode } from "react";
|
||||
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { useOrgBrandingValues } from "@calcom/features/ee/organizations/hooks";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||
@@ -205,6 +207,11 @@ function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||
}
|
||||
|
||||
function OrgBrandProvider({ children }: { children: React.ReactNode }) {
|
||||
const orgBrand = useOrgBrandingValues();
|
||||
return <OrgBrandingProvider value={orgBrand}>{children}</OrgBrandingProvider>;
|
||||
}
|
||||
|
||||
const AppProviders = (props: AppPropsWithChildren) => {
|
||||
const session = trpc.viewer.public.session.useQuery().data;
|
||||
// No need to have intercom on public pages - Good for Page Performance
|
||||
@@ -222,7 +229,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
isThemeSupported={props.Component.isThemeSupported}
|
||||
isBookingPage={props.Component.isBookingPage}>
|
||||
<FeatureFlagsProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
<OrgBrandProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</OrgBrandProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</CalcomThemeProvider>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,7 +5,25 @@ export default function useRouterQuery<T extends string>(name: T) {
|
||||
const existingQueryParams = router.asPath.split("?")[1];
|
||||
|
||||
const urlParams = new URLSearchParams(existingQueryParams);
|
||||
const query = Object.fromEntries(urlParams);
|
||||
const query: Record<string, string | string[]> = {};
|
||||
// Following error is thrown by Typescript:
|
||||
// 'Type 'URLSearchParams' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher'
|
||||
// We should change the target to higher ES2019 atleast maybe
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
for (const [key, value] of urlParams) {
|
||||
if (!query[key]) {
|
||||
query[key] = value;
|
||||
} else {
|
||||
let queryValue = query[key];
|
||||
if (queryValue instanceof Array) {
|
||||
queryValue.push(value);
|
||||
} else {
|
||||
queryValue = query[key] = [queryValue];
|
||||
queryValue.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setQuery = (newValue: string | number | null | undefined) => {
|
||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
type GetSSRResult<TProps> =
|
||||
//
|
||||
{ props: TProps } | { redirect: any } | { notFound: boolean };
|
||||
{ props: TProps } | { redirect: { destination: string; permanent: boolean } } | { notFound: boolean };
|
||||
|
||||
type GetSSRFn<TProps> = (...args: any[]) => Promise<GetSSRResult<TProps>>;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
const embed = context.query.embed;
|
||||
const layout = context.query.layout;
|
||||
|
||||
if ("redirect" in ssrResponse) {
|
||||
// Use a dummy URL https://base as the fallback base URL so that URL parsing works for relative URLs as well.
|
||||
@@ -18,6 +19,8 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||
destinationUrlObj.pathname +
|
||||
"/embed?" +
|
||||
destinationUrlObj.searchParams.toString() +
|
||||
"&layout=" +
|
||||
layout +
|
||||
"&embed=" +
|
||||
embed;
|
||||
|
||||
|
||||
+11
-7
@@ -10,6 +10,16 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
||||
const middleware: NextMiddleware = async (req) => {
|
||||
const url = req.nextUrl;
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
|
||||
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";
|
||||
return NextResponse.redirect(req.nextUrl);
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith("/api")) {
|
||||
//
|
||||
@@ -48,13 +58,6 @@ const middleware: NextMiddleware = async (req) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that embed query param is there in when /embed is added.
|
||||
// query param is the way in which client side code knows that it is in embed mode.
|
||||
if (url.pathname.endsWith("/embed") && typeof url.searchParams.get("embed") !== "string") {
|
||||
url.searchParams.set("embed", "");
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// Don't 404 old routing_forms links
|
||||
if (url.pathname.startsWith("/apps/routing_forms")) {
|
||||
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
||||
@@ -79,6 +82,7 @@ const middleware: NextMiddleware = async (req) => {
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/:path*",
|
||||
"/api/collect-events/:path*",
|
||||
"/api/auth/:path*",
|
||||
"/apps/routing_forms/:path*",
|
||||
|
||||
+207
-23
@@ -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");
|
||||
@@ -57,6 +58,19 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED
|
||||
);
|
||||
}
|
||||
|
||||
const informAboutDuplicateTranslations = () => {
|
||||
const valueSet = new Set();
|
||||
|
||||
for (const key in englishTranslation) {
|
||||
if (valueSet.has(englishTranslation[key])) {
|
||||
console.warn("\x1b[33mDuplicate value found in:", "\x1b[0m", key);
|
||||
} else {
|
||||
valueSet.add(englishTranslation[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
informAboutDuplicateTranslations();
|
||||
const plugins = [];
|
||||
if (process.env.ANALYZE === "true") {
|
||||
// only load dependency if env `ANALYZE` was set
|
||||
@@ -68,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 = {
|
||||
@@ -169,7 +219,30 @@ const nextConfig = {
|
||||
return config;
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
const beforeFiles = [
|
||||
...(process.env.ORGANIZATIONS_ENABLED
|
||||
? [
|
||||
{
|
||||
...matcherConfigRootPath,
|
||||
destination: "/team/:orgSlug",
|
||||
},
|
||||
{
|
||||
...matcherConfigOrgMemberPath,
|
||||
destination: "/org/:orgSlug/:user",
|
||||
},
|
||||
{
|
||||
...matcherConfigUserPath,
|
||||
destination: "/:user/:path*",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
let afterFiles = [
|
||||
{
|
||||
source: "/org/:slug",
|
||||
destination: "/team/:slug",
|
||||
},
|
||||
{
|
||||
source: "/:user/avatar.png",
|
||||
destination: "/api/user/avatar?username=:user",
|
||||
@@ -201,21 +274,88 @@ const nextConfig = {
|
||||
source: "/cancel/:path*",
|
||||
destination: "/booking/:path*",
|
||||
},
|
||||
// Keep cookie based booker enabled just in case we disable new-booker globally
|
||||
...[
|
||||
{
|
||||
source: userTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: teamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: privateLinkRouteRegExp,
|
||||
destination: "/new-booker/d/:link/:slug",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
],
|
||||
// Keep cookie based booker enabled to test new-booker embed in production
|
||||
...[
|
||||
{
|
||||
source: embedUserTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type/embed",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: embedTeamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type/embed",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
],
|
||||
/* TODO: have these files being served from another deployment or CDN {
|
||||
source: "/embed/embed.js",
|
||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||
}, */
|
||||
{
|
||||
source: `/:user((?!${pages.join("|")}).*)/:type`,
|
||||
destination: "/new-booker/:user/:type",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
{
|
||||
source: "/team/:slug/:type",
|
||||
destination: "/new-booker/team/:slug/:type",
|
||||
has: [{ type: "cookie", key: "new-booker-enabled" }],
|
||||
},
|
||||
|
||||
/**
|
||||
* Enables new booker using cookie. It works even if NEW_BOOKER_ENABLED_FOR_NON_EMBED, NEW_BOOKER_ENABLED_FOR_EMBED are disabled
|
||||
*/
|
||||
];
|
||||
|
||||
// Enable New Booker for all Embed Requests
|
||||
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
|
||||
console.log("Enabling New Booker for Embed");
|
||||
afterFiles.push(
|
||||
...[
|
||||
{
|
||||
source: embedUserTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type/embed",
|
||||
},
|
||||
{
|
||||
source: embedTeamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type/embed",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 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");
|
||||
afterFiles.push(
|
||||
...[
|
||||
{
|
||||
source: userTypeRouteRegExp,
|
||||
destination: "/new-booker/:user/:type",
|
||||
},
|
||||
{
|
||||
source: teamTypeRouteRegExp,
|
||||
destination: "/new-booker/team/:slug/:type",
|
||||
},
|
||||
{
|
||||
source: privateLinkRouteRegExp,
|
||||
destination: "/new-booker/d/:link/:slug",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
return {
|
||||
beforeFiles,
|
||||
afterFiles,
|
||||
};
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
@@ -250,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() {
|
||||
@@ -274,6 +443,11 @@ const nextConfig = {
|
||||
destination: "/teams",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/settings/admin",
|
||||
destination: "/settings/admin/flags",
|
||||
permanent: true,
|
||||
},
|
||||
/* V2 testers get redirected to the new settings */
|
||||
{
|
||||
source: "/settings/profile",
|
||||
@@ -319,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": "2.9.5.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;
|
||||
+91
-43
@@ -3,22 +3,37 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { orgDomainConfig, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DOCS_URL, JOIN_SLACK, 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, Slack, 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>
|
||||
@@ -202,13 +238,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 +253,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 +280,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,33 +302,37 @@ 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}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user