Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e73c4f7fb | ||
|
|
f4ea385c7f | ||
|
|
cb1d881cc0 | ||
|
+1 |
27c5bf9dbc | ||
|
|
ef7dd5102b | ||
|
|
0c3e309596 | ||
|
|
1b942c4003 | ||
|
|
b328527434 | ||
|
|
662b437a18 | ||
|
|
e7f1413ed8 | ||
|
|
071b20b6ec | ||
|
|
bf6dd6cb32 | ||
|
|
c44e444271 | ||
|
|
a06c564517 | ||
|
|
02b5a2bc1f | ||
|
|
6e161da35b | ||
|
|
b257cdbf1e | ||
|
|
9de723d557 | ||
|
|
081d5ae9c8 | ||
|
|
cddca2b10d | ||
|
|
6ed0e5e281 | ||
|
|
1fd8d50e52 | ||
|
|
5206a37fe4 | ||
|
|
025019f789 | ||
|
|
d58bd02cab | ||
|
|
b8acf1e180 | ||
|
|
d8fab68edd | ||
|
|
592d46f758 | ||
|
|
a331d02668 | ||
|
|
73e38384e2 | ||
|
|
f5b5150cf3 | ||
|
|
7e0dbee2ca | ||
|
|
46715a6cd4 | ||
|
|
1e5cb0bbd0 | ||
|
|
3a54ca2c95 | ||
|
|
79d37676fc | ||
|
|
4f76654813 | ||
|
|
6dcb0c43ab | ||
|
|
cf9116b5bb | ||
|
|
4df54ef3d1 | ||
|
|
82c98a8b7b | ||
|
|
0d7be1886e | ||
|
|
bc6c9e1b0a | ||
|
|
732acb5b25 | ||
|
|
5b6818a3cc | ||
|
|
497d4902af | ||
|
|
8c7579fede | ||
|
|
4ce6b91a79 | ||
|
|
8d0caa522e | ||
|
|
89510e6af6 | ||
|
|
297a965508 | ||
|
|
c644a10688 | ||
|
|
8ff762de3e | ||
|
|
fb2ed0b5ad | ||
|
|
358d733b5f | ||
|
|
22a4a1b2ab | ||
|
|
bcd80eab34 | ||
|
|
c3fbf8224b | ||
|
|
4cb5a56d41 | ||
|
|
5e013d5864 | ||
|
|
d90a431dcc | ||
|
|
ee02112a7c | ||
|
|
870f70fc38 | ||
|
|
cc48b87fa3 | ||
|
|
a839cf7f59 | ||
|
|
935491c70f | ||
|
|
62c168176d | ||
|
|
50a20e2c3b | ||
|
|
f28a1312a6 | ||
|
|
4c89a64f0e | ||
|
|
f57726dc95 | ||
|
|
a2a5d48037 | ||
|
|
870cb1f872 | ||
|
|
accf845f0e | ||
|
|
6d79f80928 | ||
|
|
14f0c30584 | ||
|
|
4bfa63e804 | ||
|
|
d363f6e2f6 | ||
|
|
e8d6d50bc2 | ||
|
|
9221042db4 | ||
|
|
b936b7c256 | ||
|
|
57869f0b88 | ||
|
|
7a4795bdbf | ||
|
|
773bb5424b | ||
|
|
2c600cff43 | ||
|
|
0eeee42811 | ||
|
|
8eb45ee1eb | ||
|
|
87d4afea13 | ||
|
|
7776bbbf52 | ||
|
|
88384192f8 | ||
|
|
f036f0fb64 | ||
|
|
d0f34aa699 | ||
|
+2 |
507f5d028e | ||
|
|
477c411611 | ||
|
|
3652ffa40a | ||
|
|
1332b76441 | ||
|
|
1a3aaa134e | ||
|
|
af06ded584 | ||
|
|
2f9862bb47 | ||
|
|
22d3fb7e92 | ||
|
|
3ed206df23 | ||
|
|
ff3bd5fc61 | ||
|
|
be16b72400 | ||
|
|
775fe92960 | ||
|
|
70d6c8d8e7 | ||
|
|
f589d06ed4 | ||
|
|
2b7b03fffd | ||
|
|
a53ca3389c | ||
|
|
85764b6aa6 | ||
|
|
ee767a3e41 | ||
|
|
414233961c | ||
|
|
7747b3d9a0 | ||
|
|
da638c4b1b | ||
|
|
218c3d7b50 | ||
|
|
37309e8435 | ||
|
|
2aee450c0b | ||
|
|
ca7a78dcda | ||
|
|
fc4b88e222 | ||
|
|
affedd1014 | ||
|
|
4240235cb8 | ||
|
|
2a73fd8188 | ||
|
|
e878e1a665 | ||
|
|
298a6d5e15 | ||
|
|
532723fb41 | ||
|
|
b3b82fa1cf | ||
|
|
48a1f22ca5 | ||
|
|
77bf1d0712 | ||
|
|
23c79fff75 | ||
|
|
abea19ad39 | ||
|
|
5ca2d7cae2 | ||
|
|
3ac140fad5 | ||
|
|
5358e5848d | ||
|
|
6301ea16f8 | ||
|
|
e767d233ad | ||
|
|
7480a73905 | ||
|
|
fe50b8de18 | ||
|
|
fd8b323e47 | ||
|
|
760026b2e6 | ||
|
|
53de4ddaaf | ||
|
|
e7418f68a4 | ||
|
|
3ae4655449 | ||
|
|
299f69aa7b | ||
|
|
c1896fc833 | ||
|
|
54acf6d28f | ||
|
|
7e8f13148e | ||
|
|
b42f0ffadd | ||
|
|
9346ae644f | ||
|
|
d6fd9b5df3 | ||
|
|
529adc4137 | ||
|
|
c9d7412a43 | ||
|
|
dba48ab852 | ||
|
|
0bca141b74 | ||
|
|
a4e91a0666 | ||
|
|
8efb98698d | ||
|
|
1403a567ec | ||
|
|
bd6a84e986 | ||
|
|
bd41d9d5f0 | ||
|
|
7e7938a4d9 | ||
|
|
2f03f18447 | ||
|
|
2b054862df | ||
|
|
382f11f740 | ||
|
|
920050022a | ||
|
|
95a0496f32 | ||
|
|
1ac36882ac | ||
|
|
a4b8cc8391 | ||
|
|
e7a91d4594 | ||
|
|
54c8c294fc | ||
|
|
49f584a956 | ||
|
|
67f72223bf | ||
|
|
534efe53fe | ||
|
|
5437005c7e | ||
|
|
b931cf2d7c | ||
|
|
464343f5ab | ||
|
|
84b4bb397f | ||
|
|
b90cf96ab9 | ||
|
|
5d61d2d79b | ||
|
|
1be5510c5e | ||
|
|
e871781079 | ||
|
|
bfa70dcc83 | ||
|
|
b3bd8c1a58 | ||
|
|
2c23949cea | ||
|
|
6e3825d95a | ||
|
|
525ce0d075 | ||
|
|
7537ea5b91 | ||
|
|
47468798fd | ||
|
|
587526186f | ||
|
|
7582d93d6a | ||
|
|
b9645b4de5 | ||
|
|
a1fcdcae44 | ||
|
|
cf5912335e | ||
|
|
5ad25a692f | ||
|
|
3bdf4a84f8 | ||
|
|
7a306900ab | ||
|
|
35259b75f6 | ||
|
|
538c5dff43 | ||
|
|
590ec3bf23 | ||
|
|
53befd7c5c | ||
|
|
047d4a0773 | ||
|
|
ef6ed6050b | ||
|
|
2b5759e8bb | ||
|
|
33e79c8ad5 | ||
|
|
629131b7f9 | ||
|
|
a156a784a1 | ||
|
|
d8b2fc94b6 | ||
|
|
e21e622096 | ||
|
|
160ccc3502 | ||
|
|
82d8db5f70 | ||
|
|
43a6e3f5ce | ||
|
|
c03ca5549f | ||
|
|
e395c9297d | ||
|
|
277b0c4c92 | ||
|
|
dd9adff32a | ||
|
|
b8a807da59 | ||
|
|
342efaed31 | ||
|
|
982ca3a783 | ||
|
|
9447f16b82 | ||
|
|
e82bcb6dba | ||
|
|
c0c0bbccf7 | ||
|
|
e04cb8fc61 | ||
|
|
9b5d311136 | ||
|
|
c2d6c30221 | ||
|
|
9709ee4e71 | ||
|
|
da29b8893a | ||
|
|
b94f78f6e1 | ||
|
|
0a125b6900 | ||
|
|
ee14423f4c | ||
|
|
c93e238e15 | ||
|
|
d3fcb8bf7d | ||
|
|
471420c1d4 | ||
|
|
2eedf74eb7 | ||
|
|
5a9ca309d3 | ||
|
|
12c33ea88e | ||
|
|
faac59fb8e | ||
|
|
c500952dca | ||
|
|
0a336df162 | ||
|
|
5a0cef7fb9 | ||
|
|
13d13c71c6 | ||
|
|
3ff43520eb | ||
|
|
4874a6e34c | ||
|
|
5b7cd476a7 | ||
|
|
b47c75304e | ||
|
|
be799e35d5 | ||
|
|
e35b9155d2 | ||
|
|
d1d467d28d | ||
|
|
b2e5ac4cda | ||
|
|
4d2f134dc7 | ||
|
|
aa166190e9 | ||
|
|
0e9d754f64 | ||
|
|
3b7f18cb5d | ||
|
|
63660a37f9 | ||
|
|
805419d58b | ||
|
|
0923b83eaf | ||
|
|
f40b035ff8 | ||
|
|
2b7f7a06ac | ||
|
|
baab6ecb2e | ||
|
|
9861156edb | ||
|
|
df3e3e8237 | ||
|
|
ef36a93a55 | ||
|
|
6fc3e17b47 | ||
|
|
a9ad9a6a39 | ||
|
|
1b09b7efd4 | ||
|
|
f43bfd308b | ||
|
|
4dec3fb97c | ||
|
|
ec07f84d1e | ||
|
|
07965b2a32 | ||
|
|
4c2f6df94d | ||
|
|
48916cf85e | ||
|
|
5e81d9fb56 | ||
|
|
77bf376c94 | ||
|
|
200a704756 | ||
|
|
6dd63ee3da | ||
|
|
e99b018d3b | ||
|
|
8f020bf1d0 | ||
|
|
78e2db6d70 | ||
|
|
ff6a7bf2a6 | ||
|
|
0433b0a69a | ||
|
|
ee552d8d64 | ||
|
|
dda5750bbc | ||
|
|
18389f459d | ||
|
|
fd16d734fb | ||
|
|
55152141ee | ||
|
|
75b817531e | ||
|
|
9162d90504 | ||
|
|
bf2843c17e | ||
|
|
6a28396499 | ||
|
|
1a0a318a55 | ||
|
|
99b821edcd | ||
|
|
ded0b1410a | ||
|
|
58d1c28e9d | ||
|
|
7ec5f01647 | ||
|
|
6f073762fa | ||
|
|
ebd4750f2d | ||
|
|
b1e4c23adc | ||
|
|
03b8643d1e | ||
|
|
318ec316d5 | ||
|
|
bf7c86ffe5 | ||
|
|
33e36e5320 | ||
|
|
39199e515e | ||
|
|
44b8cedef0 | ||
|
|
c7d220e998 | ||
|
|
78f0b095ae | ||
|
|
9a0b1832c3 | ||
|
|
c8ef235916 | ||
|
|
9760a2606e | ||
|
|
e916c67415 | ||
|
|
f6684c001c | ||
|
|
9e5b1f121f | ||
|
|
179da11f4b | ||
|
|
4bbca06bea | ||
|
|
c5263efce2 | ||
|
|
578c3d64ef | ||
|
|
dbca46d677 | ||
|
|
1f953c5ca0 | ||
|
|
a677ffb2a5 | ||
|
|
0ff6d4557a | ||
|
|
7bcc5c4548 | ||
|
|
dbfc1477aa | ||
|
|
b1d637d1a6 | ||
|
|
c3c7f82841 | ||
|
|
8d40e7cfd2 | ||
|
|
46266781b5 |
@@ -88,3 +88,11 @@ VITAL_REGION="us"
|
||||
# @see https://github.com/calcom/cal.com/blob/main/packages/app-store/zapier/README.md
|
||||
ZAPIER_INVITE_LINK=""
|
||||
# *********************************************************************************************************
|
||||
|
||||
# - LARK
|
||||
# Needed to enable Lark Calendar integration and Login with Lark
|
||||
# @see <https://open.larksuite.com/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g>
|
||||
LARK_OPEN_APP_ID=""
|
||||
LARK_OPEN_APP_SECRET=""
|
||||
LARK_OPEN_VERIFICATION_TOKEN=""
|
||||
# *********************************************************************************************************
|
||||
|
||||
+16
-2
@@ -21,7 +21,7 @@ CALCOM_LICENSE_KEY=
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
# ⚠️ ⚠️ ⚠️ DATABASE_URL got moved to `packages/prisma/.env.example` ⚠️ ⚠️ ⚠️
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
@@ -52,7 +52,7 @@ NEXTAUTH_URL=
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
NEXTAUTH_SECRET=
|
||||
# Used for cross-domain cookie authentication
|
||||
NEXTAUTH_COOKIE_DOMAIN=.example.com
|
||||
NEXTAUTH_COOKIE_DOMAIN=
|
||||
|
||||
# Set this to '1' if you don't want Cal to collect anonymous usage
|
||||
CALCOM_TELEMETRY_DISABLED=
|
||||
@@ -77,6 +77,17 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
# Sengrid
|
||||
# Used for email reminders in workflows
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_EMAIL=
|
||||
|
||||
# Twilio
|
||||
# Used to send SMS reminders in workflows
|
||||
TWILIO_SID=
|
||||
TWILIO_TOKEN=
|
||||
TWILIO_MESSAGING_SID=
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
NEXT_PUBLIC_IS_E2E=
|
||||
@@ -116,3 +127,6 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
|
||||
## @see https://support.google.com/accounts/answer/185833
|
||||
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
|
||||
# **********************************************************************************************************
|
||||
|
||||
# Set the following value to true if you wish to enable Team Impersonation
|
||||
NEXT_PUBLIC_TEAM_IMPERSONATION=false
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# .github/workflows/chromatic.yml
|
||||
|
||||
# Workflow name
|
||||
name: 'Chromatic'
|
||||
|
||||
# Event for the workflow
|
||||
on:
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- apps/storybook/**
|
||||
- packages/ui/**
|
||||
workflow_dispatch:
|
||||
|
||||
# List of jobs
|
||||
jobs:
|
||||
chromatic-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
# 👇 Adds Chromatic as a step in the workflow
|
||||
- name: Publish to Chromatic
|
||||
uses: chromaui/action@v1
|
||||
# Options required to the GitHub Chromatic Action
|
||||
with:
|
||||
# 👇 Chromatic projectToken, refer to the manage page to obtain it.
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
workingDir: "apps/storybook"
|
||||
buildScriptName: "build"
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Check types
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Cleanup Preview Databases
|
||||
|
||||
env:
|
||||
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||
SNAPLET_PROJECT_ID: cl4u26bwz7962859ply7ibuo43t
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: snaplet/action@v1
|
||||
if: ${{ env.SNAPLET_ACCESS_TOKEN && env.SNAPLET_PROJECT_ID }}
|
||||
with:
|
||||
delete: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Cron - scheduleEmailReminders
|
||||
|
||||
on:
|
||||
# "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 “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
jobs:
|
||||
cron-scheduleEmailReminders:
|
||||
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/workflows/scheduleEmailReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Cron - scheduleSMSReminders
|
||||
|
||||
on:
|
||||
# "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 “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
jobs:
|
||||
cron-scheduleSMSReminders:
|
||||
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/workflows/scheduleSMSReminders \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_API_KEY }}' \
|
||||
--fail
|
||||
@@ -0,0 +1,99 @@
|
||||
name: E2E App-Store Apps
|
||||
on:
|
||||
push:
|
||||
branches: [ feature/event-routing ]
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- apps/api/**
|
||||
- apps/console/**
|
||||
- apps/docs/**
|
||||
- apps/swagger/**
|
||||
- apps/website/**
|
||||
- apps/web/public/**
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 20
|
||||
name: E2E App-Store Apps
|
||||
strategy:
|
||||
matrix:
|
||||
node: ["16.x"]
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
|
||||
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: secret
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
GOOGLE_LOGIN_ENABLED: true
|
||||
# CRON_API_KEY: xxx
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||
PAYMENT_FEE_FIXED: 10
|
||||
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
SAML_ADMINS: pro@example.com
|
||||
NEXTAUTH_URL: http://localhost:3000/api/auth
|
||||
NEXT_PUBLIC_IS_E2E: 1
|
||||
# EMAIL_FROM: e2e@cal.com
|
||||
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
|
||||
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
|
||||
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
|
||||
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
|
||||
# MS_GRAPH_CLIENT_ID: xxx
|
||||
# MS_GRAPH_CLIENT_SECRET: xxx
|
||||
# ZOOM_CLIENT_ID: xxx
|
||||
# ZOOM_CLIENT_SECRET: xxx
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.1
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: calendso
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
|
||||
fetch-depth: 2
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
|
||||
- name: Use Node ${{ matrix.node }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/ms-playwright
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: cache-playwright-
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Install playwright deps
|
||||
# if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
run: yarn playwright install --with-deps
|
||||
- name: Run Tests
|
||||
run: yarn app-e2e-quick
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results-core
|
||||
path: packages/app-store/**/playwright/results
|
||||
@@ -1,6 +1,6 @@
|
||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||
on:
|
||||
pull_request: # So we can test on forks
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: E2E test
|
||||
on:
|
||||
pull_request: # So we can test on forks
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
|
||||
@@ -10,5 +10,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
||||
if: ${{ github.actor == 'github-actions[bot]' }}
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Lint
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
@@ -35,10 +35,11 @@ jobs:
|
||||
run: jq -s '[.[]]|flatten' lint-results/*.json &> lint-results/eslint_report.json
|
||||
|
||||
- name: Annotate Code Linting Results
|
||||
uses: ataylorme/eslint-annotate-action@1.2.0
|
||||
uses: ataylorme/eslint-annotate-action@v2
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
report-json: "lint-results/eslint_report.json"
|
||||
only-pr-files: false
|
||||
|
||||
- name: Upload ESLint report
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -3,7 +3,6 @@ on:
|
||||
schedule:
|
||||
- cron: "15 */4 * * *"
|
||||
workflow_dispatch: ~
|
||||
|
||||
jobs:
|
||||
submodule-sync:
|
||||
name: Submodule update
|
||||
@@ -11,11 +10,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: run action
|
||||
uses: releasehub-com/github-action-create-pr-parent-submodule@v1
|
||||
with:
|
||||
github_token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
parent_repository: "calcom/cal.com"
|
||||
checkout_branch: "main"
|
||||
pr_against_branch: "main"
|
||||
owner: "calcom"
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Commit
|
||||
run: |
|
||||
git config user.email "actions@github.com"
|
||||
git config user.name "github-actions"
|
||||
git commit -am "Auto updated submodule references" && git push || echo "No changes to commit"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# .env file
|
||||
.env
|
||||
!packages/prisma/.env
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
@@ -73,6 +74,9 @@ dist
|
||||
# Linting
|
||||
lint-results
|
||||
|
||||
#Storybook
|
||||
apps/storybook/build-storybook.log
|
||||
|
||||
# Snaplet
|
||||
.snaplet/snapshots
|
||||
.snaplet/structure.d.ts
|
||||
|
||||
Vendored
-4
@@ -22,10 +22,6 @@
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
// Try start the task on folder open
|
||||
"runOptions": {
|
||||
"runOn": "folderOpen"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under "apps/web/ee/" (https://github.com/calcom/cal.com/tree/main/apps/web/ee) directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
|
||||
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) 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.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<p align="center">
|
||||
<a href="https://cal.com/slack"><img src="https://img.shields.io/badge/Slack-calendso.slack.com-%234A154B" alt="Join Cal.com Slack"></a>
|
||||
<a href="https://www.producthunt.com/posts/calendso"><img src="https://img.shields.io/badge/Product%20Hunt-%231%20Product%20of%20the%20Month-%23DA552E" alt="Product Hunt"></a>
|
||||
<a href="https://status.cal.com"><img src="https://betteruptime.com/status-badges/v1/monitor/a9kf.svg" alt="Uptime"></a>
|
||||
<a href="https://github.com/calcom/cal.com/stargazers"><img src="https://img.shields.io/github/stars/calcom/cal.com" alt="Github Stars"></a>
|
||||
<a href="https://news.ycombinator.com/item?id=26817795"><img src="https://img.shields.io/badge/Hacker%20News-311-%23FF6600" alt="Hacker News"></a>
|
||||
<a href="https://github.com/calcom/cal.com/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-purple" alt="License"></a>
|
||||
@@ -36,6 +37,8 @@
|
||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||
<a href="https://github.com/orgs/calcom/projects/1/views/25"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||
</p>
|
||||
@@ -84,7 +87,7 @@ To get a local copy up and running, please follow these simple steps.
|
||||
|
||||
Here is what you need to be able to run Cal.
|
||||
|
||||
- Node.js (Version: >=14.x <17)
|
||||
- Node.js (Version: >=15.x <17)
|
||||
- PostgreSQL
|
||||
- Yarn _(recommended)_
|
||||
|
||||
@@ -133,7 +136,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
|
||||
#### Manual setup
|
||||
|
||||
1. Configure environment variables in the `packages/prisma/.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||
1. Configure environment variables in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||
|
||||
```
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
@@ -167,7 +170,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
</details>
|
||||
|
||||
1. Set a 32 character random string in your .env file for the `CALENDSO_ENCRYPTION_KEY` (You can use a command like `openssl rand -base64 24` to generate one).
|
||||
1. Set up the database using the Prisma schema (found in `apps/web/prisma/schema.prisma`)
|
||||
1. Set up the database using the Prisma schema (found in `packages/prisma/schema.prisma`)
|
||||
|
||||
```sh
|
||||
yarn workspace @calcom/prisma db-deploy
|
||||
@@ -189,7 +192,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||
|
||||
1. Click on the `User` model to add a new user record.
|
||||
1. Fill out the fields `email`, `username`, `password`, and set `metadata` to empty `{}` (remembering to encrypt your password with [BCrypt](https://bcrypt-generator.com/)) and click `Save 1 Record` to create your first user.
|
||||
> New users are set on a `TRIAL` plan by default. You might want to adjust this behavior to your needs in the `apps/web/prisma/schema.prisma` file.
|
||||
> New users are set on a `TRIAL` plan by default. You might want to adjust this behavior to your needs in the `packages/prisma/schema.prisma` file.
|
||||
1. Open a browser to [http://localhost:3000](http://localhost:3000) and login with your just created, first user.
|
||||
|
||||
### E2E-Testing
|
||||
@@ -260,12 +263,16 @@ yarn workspace @calcom/web playwright-report
|
||||
|
||||
### Docker
|
||||
|
||||
The Docker configuration for Cal is an effort powered by people within the community. Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
||||
The Docker configuration for Cal is an effort powered by people within the community.
|
||||
|
||||
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
||||
|
||||
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
|
||||
|
||||
Issues with Docker? Find your answer or open a new discussion [here](https://github.com/calcom/docker/discussions) to ask the community.
|
||||
|
||||
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">
|
||||
@@ -274,20 +281,21 @@ The Docker configuration can be found [in our docker repository](https://github.
|
||||
|
||||
### Railway
|
||||
|
||||
[](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fcalendso%2Fcalendso&plugins=postgresql&envs=GOOGLE_API_CREDENTIALS%2CBASE_URL%2CNEXTAUTH_URL%2CPORT&BASE_URLDefault=http%3A%2F%2Flocalhost%3A3000&NEXTAUTH_URLDefault=http%3A%2F%2Flocalhost%3A3000&PORTDefault=3000)
|
||||
[](https://railway.app/new/template/cal)
|
||||
|
||||
You can deploy Cal on [Railway](https://railway.app/) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal on their platform.
|
||||
|
||||
### Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_SECRET,NEXTAUTH_URL,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT,CALCOM_LICENSE_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com)
|
||||
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
|
||||
|
||||
<!-- ROADMAP -->
|
||||
|
||||
## Roadmap
|
||||
|
||||
<a href="https://cal.com/roadmap"><img src="https://user-images.githubusercontent.com/8019099/176388295-25081ca4-207e-4468-8079-37b195fa8e59.png" alt="Cal.com Roadmap" /></a>
|
||||
|
||||
<a href="https://cal.com/roadmap"><img src="https://user-images.githubusercontent.com/8019099/176388295-25081ca4-207e-4468-8079-37b195fa8e59.png" alt="Cal.com Roadmap" /></a>
|
||||
|
||||
See the [roadmap project](https://cal.com/roadmap) for a list of proposed features (and known issues). You can change the view to see planned tagged releases.
|
||||
|
||||
@@ -299,7 +307,7 @@ Please see our [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
### Good First Issues
|
||||
|
||||
We have a list of [good first issues](https://github.com/calcom/cal.com/labels/✅%20good%20first%20issue) that contain bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||
We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/25) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
|
||||
|
||||
## Integrations
|
||||
|
||||
@@ -317,6 +325,23 @@ We have a list of [good first issues](https://github.com/calcom/cal.com/labels/
|
||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
||||
|
||||
#### _Adding google calendar to Cal.com App Store_
|
||||
|
||||
After adding Google credentials, you can now Google Calendar App to the app store.
|
||||
You can repopulate the App store by running
|
||||
|
||||
```
|
||||
cd packages/prisma
|
||||
yarn seed-app-store
|
||||
```
|
||||
|
||||
You will need to complete a few more steps to activate Google Calendar App.
|
||||
Make sure to complete section "Obtaining the Google API Credentials". After the do the
|
||||
following
|
||||
|
||||
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
|
||||
1. Under 'OAuth concent screen', click "PUBLISH APP"
|
||||
|
||||
### Obtaining Microsoft Graph Client ID and Secret
|
||||
|
||||
1. Open [Azure App Registration](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and select New registration
|
||||
@@ -425,6 +450,31 @@ Next make sure you have your app running `yarn dx`. Then in the slack chat type
|
||||
1. Select all events for the webhook you interested, e.g. `sleep_created`
|
||||
1. Copy the webhook secret (`sec...`) to `VITAL_WEBHOOK_SECRET` in the .env.appStore file.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Setting up SendGrid for Email reminders
|
||||
|
||||
1. Create a SendGrid account (https://signup.sendgrid.com/)
|
||||
2. Go to Settings -> API keys and create an API key
|
||||
3. Copy API key to your .env file into the SENDGRID_API_KEY field
|
||||
4. Go to Settings -> Sender Authentication and verify a single sender
|
||||
5. Copy the verified E-Mail to your .env file into the SENDGRID_EMAIL field
|
||||
|
||||
### Setting up Twilio for SMS reminders
|
||||
|
||||
1. Create a Twilio account (https://www.twilio.com/try-twilio)
|
||||
2. Click ‘Get a Twilio phone number’
|
||||
3. Copy Account SID to your .env file into the TWILIO_SID field
|
||||
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
|
||||
5. Create a messaging service (Develop -> Messaging -> Services)
|
||||
6. Choose any name for the messaging service
|
||||
7. Click 'Add Senders'
|
||||
8. Choose phone number as sender type
|
||||
9. Add the listed phone number
|
||||
10. Leave all other fields as they are
|
||||
11. Complete setup and click ‘View my new Messaging Service’
|
||||
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
## License
|
||||
|
||||
+22
-21
@@ -1,4 +1,5 @@
|
||||
# Security
|
||||
|
||||
Contact: security@cal.com
|
||||
|
||||
Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt)
|
||||
@@ -9,30 +10,30 @@ If you discover a vulnerability, we would like to know about it so we can take s
|
||||
|
||||
## 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.
|
||||
* Email spoofing
|
||||
* Missing DNSSEC, CAA, CSP headers
|
||||
* Lack of Secure or HTTP only flag on non-sensitive cookies
|
||||
* Deadlinks
|
||||
- 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.
|
||||
- Email spoofing
|
||||
- Missing DNSSEC, CAA, CSP headers
|
||||
- Lack of Secure or HTTP only flag on non-sensitive cookies
|
||||
- Deadlinks
|
||||
|
||||
## 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 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.
|
||||
- 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 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.
|
||||
|
||||
## 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 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.
|
||||
- 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.
|
||||
|
||||
@@ -10,18 +10,73 @@
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"BASE_URL": {
|
||||
"PGSSLMODE": {
|
||||
"description": "If you use Heroku to deploy Postgres (or use self-signed certs for Postgres) then put 'no-verify' here.",
|
||||
"value": "no-verify"
|
||||
},
|
||||
"NEXT_PUBLIC_WEBAPP_URL": {
|
||||
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
||||
"value": "https://HEROKU_APP_NAME.herokuapp.com"
|
||||
"value": "https://HEROKU_APP_NAME.herokuapp.com",
|
||||
"required": "true"
|
||||
},
|
||||
"CALENDSO_ENCRYPTION_KEY": {
|
||||
"description": "Application Key for symmetric encryption and decryption. Must be 32 bytes for AES256 encryption algorithm.",
|
||||
"value": "secret"
|
||||
"value": "secret",
|
||||
"required": "true"
|
||||
},
|
||||
"NEXTAUTH_SECRET": "secret",
|
||||
"NEXTAUTH_URL": {
|
||||
"description": "Replace HEROKU_APP_NAME with the name given to your app",
|
||||
"value": "https://HEROKU_APP_NAME.herokuapp.com"
|
||||
},
|
||||
"NEXTAUTH_SECRET": {
|
||||
"description": "Cookie encryption key",
|
||||
"generator": "secret",
|
||||
"required": "true"
|
||||
},
|
||||
"NEXT_PUBLIC_LICENSE_CONSENT": {
|
||||
"description": "License consent",
|
||||
"value": "true",
|
||||
"required": "true"
|
||||
},
|
||||
"CRON_API_KEY": {
|
||||
"description": "ApiKey for cronjobs",
|
||||
"value": ""
|
||||
},
|
||||
"SEND_FEEDBACK_EMAIL": {
|
||||
"description": "Send feedback email",
|
||||
"value": ""
|
||||
},
|
||||
"SENDGRID_API_KEY": {
|
||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
},
|
||||
"SENDGRID_EMAIL": {
|
||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_FROM": {
|
||||
"description": "Configures the global From: header whilst sending emails",
|
||||
"value": ""
|
||||
},
|
||||
"EMAIL_SERVER_HOST": {
|
||||
"description": "Configures the global SMTP server host",
|
||||
"value": "smtp.gmail.com"
|
||||
},
|
||||
"EMAIL_SERVER_PORT": {
|
||||
"description": "Configures the global SMTP server port",
|
||||
"value": "465"
|
||||
},
|
||||
"EMAIL_SERVER_USER": {
|
||||
"description": "Configures the global SMTP server user",
|
||||
"value": "<gmail_emailAddress>"
|
||||
},
|
||||
"EMAIL_SERVER_PASSWORD": {
|
||||
"description": "Configures the global SMTP server password",
|
||||
"value": "<gmail_app_password>"
|
||||
},
|
||||
"NEXT_PUBLIC_TEAM_IMPERSONATION": {
|
||||
"description": "Set the following value to true if you wish to enable Team Impersonation",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
+1
-1
Submodule apps/api updated: 74c227f5f1...181921f9c7
+1
-1
Submodule apps/console updated: 74bd9e2831...3e84e0bac7
@@ -4,26 +4,27 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "PORT=4000 next",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||
"start": "PORT=4000 next start",
|
||||
"build": "next build"
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
},
|
||||
"author": "Cal.com, Inc.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iframe-resizer-react": "^1.1.0",
|
||||
"next": "^12.1.6",
|
||||
"next": "^12.2.5",
|
||||
"nextra": "^1.1.0",
|
||||
"nextra-theme-docs": "^1.2.2",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0"
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/config": "*",
|
||||
"eslint": "^8.15.0"
|
||||
"eslint": "^8.20.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ We use a number of style guides written by other amazing companies, simply becau
|
||||
We don't expect you to study every single rule of each style guide, but these are great reference points as to how your code should be styled. We may reject a pull request if your code style significantly differs from these style guides however.
|
||||
|
||||
## ESLint & Prettier
|
||||
Calendso uses the ESLint and Prettier formatting tools, and the repository comes with defined rules for each tool. We recommend setting up both tools and using these to help automatically style your code to our guidelines.
|
||||
Cal.com uses the ESLint and Prettier formatting tools, and the repository comes with defined rules for each tool. We recommend setting up both tools and using these to help automatically style your code to our guidelines.
|
||||
|
||||
## JavaScript/TypeScript
|
||||
We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for all JavaScript and Typescript code.
|
||||
We use the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) for all JavaScript and TypeScript code.
|
||||
|
||||
## HTML & CSS
|
||||
We use the [Google HTML/CSS Style Guide](https://google.github.io/styleguide/htmlcssguide.html) for any HTML and CSS markup. However, exceptions to the HTML guide apply where JSX differentiates from standard HTML.
|
||||
|
||||
@@ -34,7 +34,6 @@ Here is what you need to be able to run Cal.
|
||||
|
||||
```sh
|
||||
cp apps/web/.env.example apps/web/.env
|
||||
cp packages/prisma/.env.example packages/prisma/.env
|
||||
```
|
||||
|
||||
1. Install packages with yarn
|
||||
@@ -54,7 +53,7 @@ yarn dx
|
||||
|
||||
### Manual setup
|
||||
|
||||
1. Configure database in the `packages/prisma/.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||
1. Configure database in the `.env` file. Replace `<user>`, `<pass>`, `<db-host>`, `<db-port>` with their applicable values
|
||||
|
||||
```text
|
||||
DATABASE_URL='postgresql://<user>:<pass>@<db-host>:<db-port>'
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
storybook-static/
|
||||
@@ -0,0 +1,57 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-designs",
|
||||
"@storybook/addon-a11y",
|
||||
"storybook-addon-next",
|
||||
],
|
||||
framework: "@storybook/react",
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
/**
|
||||
* Fixes font import with /
|
||||
* @see https://github.com/storybookjs/storybook/issues/12844#issuecomment-867544160
|
||||
*/
|
||||
config.resolve.roots = [path.resolve(__dirname, "../public"), "node_modules"];
|
||||
|
||||
/**
|
||||
* Why webpack5... Just why?
|
||||
* @type {{console: boolean, process: boolean, timers: boolean, os: boolean, querystring: boolean, sys: boolean, fs: boolean, url: boolean, crypto: boolean, path: boolean, zlib: boolean, punycode: boolean, util: boolean, stream: boolean, assert: boolean, string_decoder: boolean, domain: boolean, vm: boolean, tty: boolean, http: boolean, buffer: boolean, constants: boolean, https: boolean, events: boolean}}
|
||||
*/
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
buffer: false,
|
||||
console: false,
|
||||
constants: false,
|
||||
crypto: false,
|
||||
domain: false,
|
||||
events: false,
|
||||
http: false,
|
||||
https: false,
|
||||
os: false,
|
||||
path: false,
|
||||
punycode: false,
|
||||
process: false,
|
||||
querystring: false,
|
||||
stream: false,
|
||||
string_decoder: false,
|
||||
sys: false,
|
||||
timers: false,
|
||||
tty: false,
|
||||
url: false,
|
||||
util: false,
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
<script>
|
||||
window.isEmbed = ()=> {
|
||||
return location.search.includes("embed=")
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap');
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { RouterContext } from "next/dist/shared/lib/router-context";
|
||||
import * as NextImage from "next/image";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const OriginalNextImage = NextImage.default;
|
||||
|
||||
Object.defineProperty(NextImage, "default", {
|
||||
configurable: true,
|
||||
value: (props) => <OriginalNextImage {...props} unoptimized />,
|
||||
});
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
# Storybook & UI
|
||||
|
||||
Storybook is home to all of our commonly used components. We use this app to visually show all components that we have within the project + automatic type documentation generated for each component.
|
||||
|
||||
All changes to storybook/ui must require a visual/code review.
|
||||
|
||||
- Visual reviews happen on a tool called [Chormatic](http://chromatic.com/) and these reviews will happen by our product team.
|
||||
|
||||
- Code reviews happen as normal on any changes to storybook/ui designs
|
||||
|
||||
## Deployment
|
||||
|
||||
To deploy this project run
|
||||
|
||||
```bash
|
||||
cd apps/storybook
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
const withTM = require("next-transpile-modules")(["@calcom/dayjs", "@calcom/ui"]);
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = withTM(nextConfig);
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@calcom/storybook",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:next": "next dev",
|
||||
"build:next": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/storybook.json",
|
||||
"dev": "start-storybook -p 6006",
|
||||
"build": "build-storybook",
|
||||
"chromatic": "npx chromatic --project-token=56fadc9ab496"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^0.1.4",
|
||||
"@radix-ui/react-collapsible": "^0.1.0",
|
||||
"@radix-ui/react-dialog": "^0.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
||||
"@radix-ui/react-id": "^0.1.0",
|
||||
"@radix-ui/react-radio-group": "^0.1.1",
|
||||
"@radix-ui/react-slider": "^0.1.1",
|
||||
"@radix-ui/react-switch": "^0.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"next": "^12.2.5",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-hot-toast": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.9",
|
||||
"@calcom/config": "*",
|
||||
"@storybook/addon-a11y": "^6.5.9",
|
||||
"@storybook/addon-actions": "^6.5.9",
|
||||
"@storybook/addon-essentials": "^6.5.9",
|
||||
"@storybook/addon-interactions": "^6.5.9",
|
||||
"@storybook/addon-links": "^6.5.9",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.9",
|
||||
"@storybook/manager-webpack5": "^6.5.9",
|
||||
"@storybook/react": "^6.5.9",
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "18.0.4",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-loader": "^8.2.5",
|
||||
"chromatic": "^6.7.1",
|
||||
"eslint": "^8.20.0",
|
||||
"postcss": "^8.4.13",
|
||||
"postcss-loader": "^7.0.0",
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.7",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"_id": "@calcom/storybook@0.1.0"
|
||||
}
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import type { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className={styles.main}>
|
||||
<h1 className={styles.title}>
|
||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
||||
</h1>
|
||||
|
||||
<p className={styles.description}>
|
||||
Get started by editing <code className={styles.code}>pages/index.tsx</code>
|
||||
</p>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
||||
<h2>Documentation →</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
||||
<h2>Learn →</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/vercel/next.js/tree/canary/examples" className={styles.card}>
|
||||
<h2>Examples →</h2>
|
||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}>
|
||||
<h2>Deploy →</h2>
|
||||
<p>Instantly deploy your Next.js site to a public URL with Vercel.</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className={styles.footer}>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Powered by{" "}
|
||||
<span className={styles.logo}>
|
||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
||||
</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Executable
+4
@@ -0,0 +1,4 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,26 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import Avatar from "@calcom/ui/v2/Avatar";
|
||||
|
||||
export default {
|
||||
title: "Avatar",
|
||||
component: Avatar,
|
||||
} as ComponentMeta<typeof Avatar>;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<Avatar size="sm" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" />
|
||||
<Avatar size="lg" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Accepted = () => {
|
||||
return (
|
||||
<>
|
||||
<Avatar size="sm" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" accepted />
|
||||
<Avatar size="lg" alt="Avatar Story" gravatarFallbackMd5="Ui@CAL.com" accepted />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import Avatar from "@calcom/ui/v2/Avatar";
|
||||
import AvatarGroup from "@calcom/ui/v2/AvatarGroup";
|
||||
|
||||
export default {
|
||||
title: "Avatar/Group",
|
||||
component: AvatarGroup,
|
||||
} as ComponentMeta<typeof AvatarGroup>;
|
||||
|
||||
const IMAGES = [
|
||||
{
|
||||
image: "https://cal.com/stakeholder/peer.jpg",
|
||||
alt: "Peer",
|
||||
title: "Peer Richelsen",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/bailey.jpg",
|
||||
alt: "Bailey",
|
||||
title: "Bailey Pumfleet",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/alex-van-andel.jpg",
|
||||
alt: "Alex",
|
||||
title: "Alex Van Andel",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/ciaran.jpg",
|
||||
alt: "Ciarán",
|
||||
title: "Ciarán Hanrahan",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/peer.jpg",
|
||||
alt: "Peer",
|
||||
title: "Peer Richelsen",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/bailey.jpg",
|
||||
alt: "Bailey",
|
||||
title: "Bailey Pumfleet",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/alex-van-andel.jpg",
|
||||
alt: "Alex",
|
||||
title: "Alex Van Andel",
|
||||
},
|
||||
{
|
||||
image: "https://cal.com/stakeholder/ciaran.jpg",
|
||||
alt: "Ciarán",
|
||||
title: "Ciarán Hanrahan",
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<AvatarGroup size="lg" items={IMAGES} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Bell } from "react-feather";
|
||||
|
||||
import { Badge } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Badge",
|
||||
component: Badge,
|
||||
} as ComponentMeta<typeof Badge>;
|
||||
|
||||
export const All = () => (
|
||||
<div className="">
|
||||
<h1>Default</h1>
|
||||
<div className="mb-4 flex space-x-2">
|
||||
<Badge variant="gray">Badge</Badge>
|
||||
<Badge variant="red">Badge</Badge>
|
||||
<Badge variant="green">Badge</Badge>
|
||||
<Badge variant="orange">Badge</Badge>
|
||||
<Badge variant="blue">Badge</Badge>
|
||||
</div>
|
||||
<h1>Icons</h1>
|
||||
<div className="mb-4 flex space-x-2">
|
||||
<Badge variant="gray" StartIcon={Bell}>
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="red" StartIcon={Bell}>
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="green" StartIcon={Bell}>
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="orange" StartIcon={Bell}>
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="blue" StartIcon={Bell}>
|
||||
Badge
|
||||
</Badge>
|
||||
</div>
|
||||
<h1>Large</h1>
|
||||
<div className="flex space-x-2">
|
||||
<Badge variant="gray" size="lg">
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="red" size="lg">
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="green" size="lg">
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="orange" size="lg">
|
||||
Badge
|
||||
</Badge>
|
||||
<Badge variant="blue" size="lg">
|
||||
Badge
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import { Info } from "react-feather";
|
||||
|
||||
import { Banner } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Banner",
|
||||
component: Banner,
|
||||
} as ComponentMeta<typeof Banner>;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<Banner
|
||||
variant="neutral"
|
||||
title="Summarise what happened"
|
||||
description="Describe what can be done about it here."
|
||||
Icon={Info}
|
||||
onDismiss={() => console.log("dismissed")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Warning = () => {
|
||||
return (
|
||||
<Banner
|
||||
variant="warning"
|
||||
title="Summarise what happened"
|
||||
description="Describe what can be done about it here."
|
||||
Icon={Info}
|
||||
onDismiss={() => console.log("dismissed")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = () => {
|
||||
return (
|
||||
<Banner
|
||||
variant="error"
|
||||
title="Summarise what happened"
|
||||
description="Describe what can be done about it here."
|
||||
errorMessage="Event creation failed"
|
||||
Icon={Info}
|
||||
onDismiss={() => console.log("dismissed")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Breadcrumb, BreadcrumbItem } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Breadcrumbs",
|
||||
component: Breadcrumb,
|
||||
} as ComponentMeta<typeof Breadcrumb>;
|
||||
|
||||
export const Default = () => (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem href="/">Home</BreadcrumbItem>
|
||||
<BreadcrumbItem href="/">Test</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
);
|
||||
|
||||
Default.parameters = {
|
||||
nextRouter: {
|
||||
path: "/test",
|
||||
asPath: "/test",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import { Trash2 } from "react-feather";
|
||||
|
||||
import { Button as ButtonComponent } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Button",
|
||||
component: ButtonComponent,
|
||||
argTypes: {
|
||||
color: {
|
||||
options: ["primary", "secondary", "minimal", "destructive"],
|
||||
control: { type: "select" },
|
||||
},
|
||||
disabled: {
|
||||
options: [true, false],
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
loading: {
|
||||
options: [true, false],
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
size: {
|
||||
options: ["base", "lg", "icon"],
|
||||
control: { type: "radio" },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ButtonComponent>;
|
||||
|
||||
const Template: ComponentStory<typeof ButtonComponent> = (args) => <ButtonComponent {...args} />;
|
||||
|
||||
export const All = () => (
|
||||
<div>
|
||||
<h1>Primary</h1>
|
||||
<div className="flex space-x-2">
|
||||
<ButtonComponent aria-label="Button Text">Button Text</ButtonComponent>
|
||||
<ButtonComponent disabled aria-label="Button Text">
|
||||
Button Text
|
||||
</ButtonComponent>
|
||||
</div>
|
||||
<h1>Secondary</h1>
|
||||
<div className="flex space-x-2">
|
||||
<ButtonComponent color="secondary" aria-label="Button Text">
|
||||
Button Text
|
||||
</ButtonComponent>
|
||||
<ButtonComponent disabled color="secondary" aria-label="Button Text">
|
||||
Button Text
|
||||
</ButtonComponent>
|
||||
<ButtonComponent size="icon" color="secondary" StartIcon={Trash2} aria-label="Button Text" />
|
||||
</div>
|
||||
<h1>Minimal</h1>
|
||||
<div className="flex">
|
||||
<ButtonComponent color="minimal" aria-label="Button Text">
|
||||
Button Text
|
||||
</ButtonComponent>
|
||||
<ButtonComponent disabled color="minimal" aria-label="Button Text">
|
||||
Button Text
|
||||
</ButtonComponent>
|
||||
<ButtonComponent size="icon" color="minimal" StartIcon={Trash2} aria-label="Button Text" />
|
||||
</div>
|
||||
<h1>Destructive</h1>
|
||||
<ButtonComponent size="icon" color="destructive" StartIcon={Trash2} aria-label="Button Text" />
|
||||
<h1>Tooltip</h1>
|
||||
<ButtonComponent
|
||||
tooltip="Deletes EventTypes"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
StartIcon={Trash2}
|
||||
aria-label="Button Text"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Button = Template.bind({});
|
||||
Button.args = {
|
||||
color: "primary",
|
||||
children: "Button Text",
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Button.args,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
...Button.args,
|
||||
loading: true,
|
||||
};
|
||||
export const Icon = Template.bind({});
|
||||
Icon.args = {
|
||||
color: "secondary",
|
||||
StartIcon: Trash2,
|
||||
size: "icon",
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { ArrowLeft, ArrowRight, Clipboard, Navigation2, Trash2 } from "react-feather";
|
||||
|
||||
import { Button, ButtonGroup } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Button Group",
|
||||
component: ButtonGroup,
|
||||
} as ComponentMeta<typeof ButtonGroup>;
|
||||
|
||||
export const Default = () => (
|
||||
<ButtonGroup>
|
||||
<Button StartIcon={Trash2} size="icon" color="secondary" />
|
||||
<Button StartIcon={Navigation2} size="icon" color="secondary" />
|
||||
<Button StartIcon={Clipboard} size="icon" color="secondary" />
|
||||
</ButtonGroup>
|
||||
);
|
||||
|
||||
export const Combined = () => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<ButtonGroup combined>
|
||||
<Button StartIcon={Trash2} size="icon" color="secondary" combined />
|
||||
<Button StartIcon={Navigation2} size="icon" color="secondary" combined />
|
||||
<Button StartIcon={Clipboard} size="icon" color="secondary" combined />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup combined>
|
||||
<Button StartIcon={ArrowLeft} size="icon" color="secondary" combined />
|
||||
<Button StartIcon={ArrowRight} size="icon" color="secondary" combined />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { BaseCardProps, Card } from "@calcom/ui/v2/Card";
|
||||
|
||||
export default {
|
||||
title: "Cards",
|
||||
component: Card,
|
||||
} as ComponentMeta<typeof Card>;
|
||||
|
||||
const profileProps: BaseCardProps[] = [
|
||||
{
|
||||
variant: "ProfileCard",
|
||||
title: "Alex Fisher",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
|
||||
description: "Co-founder at Attio. Book a meeting to discuss enterprise deals.",
|
||||
},
|
||||
{
|
||||
variant: "ProfileCard",
|
||||
title: "Julian Wan",
|
||||
image:
|
||||
"https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80",
|
||||
description: "Co-founder at Attio. Book a meeting to discuss enterprise deals.",
|
||||
},
|
||||
];
|
||||
|
||||
export const Profile = () => {
|
||||
return (
|
||||
<div className="flex w-full space-x-2 bg-gray-200 p-4">
|
||||
<Card {...profileProps[0]} />
|
||||
<Card {...profileProps[1]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const appStoreProps: BaseCardProps = {
|
||||
variant: "AppStore",
|
||||
title: "Giphy",
|
||||
image: "https://app.cal.com/api/app-store/giphy/icon.svg",
|
||||
description: "Add GIFs to spice up your booking confirmations.",
|
||||
actionButton: {
|
||||
href: "#",
|
||||
child: "View Details",
|
||||
},
|
||||
};
|
||||
|
||||
export const AppStore = () => {
|
||||
return (
|
||||
<div className="w-full bg-gray-200 p-4">
|
||||
<Card {...appStoreProps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Checkbox } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Checkbox",
|
||||
component: Checkbox,
|
||||
} as ComponentMeta<typeof Checkbox>;
|
||||
|
||||
export const All = () => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Checkbox label="Default" description="Toggle on and off something" />
|
||||
<Checkbox label="Error" description="Toggle on and off something" error />
|
||||
<Checkbox label="Disabled" description="Toggle on and off something" disabled />
|
||||
<Checkbox label="Disabled Checked" description="Toggle on and off something" checked disabled />
|
||||
<hr />
|
||||
<Checkbox description="Default" descriptionAsLabel />
|
||||
<Checkbox description="Error" descriptionAsLabel error />
|
||||
<Checkbox description="Disabled" descriptionAsLabel disabled />
|
||||
<Checkbox description="Disabled" descriptionAsLabel disabled checked />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CheckboxField = () => <Checkbox description="Default" descriptionAsLabel />;
|
||||
export const CheckboxError = () => <Checkbox description="Error" descriptionAsLabel />;
|
||||
export const CheckboxDisabled = () => (
|
||||
<>
|
||||
<Checkbox description="Disabled" descriptionAsLabel disabled />
|
||||
<Checkbox description="Disabled" descriptionAsLabel disabled checked />
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,208 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import ColorPicker from "@calcom/ui/v2/colorpicker";
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default {
|
||||
title: "Colors",
|
||||
component: ColorPicker,
|
||||
};
|
||||
const COLORS = {
|
||||
brand: {
|
||||
50: "#f3f3f4",
|
||||
100: "#e7e8e9",
|
||||
200: "#c4c5c9",
|
||||
300: "#a0a3a9",
|
||||
400: "#585d68",
|
||||
500: "#111827", // Brand color
|
||||
600: "#0f1623",
|
||||
700: "#0d121d",
|
||||
800: "#0a0e17",
|
||||
900: "#080c13",
|
||||
},
|
||||
gray: {
|
||||
50: "#F8F8F8",
|
||||
100: "#F5F5F5",
|
||||
200: "#E1E1E1",
|
||||
300: "#CFCFCF",
|
||||
400: "#ACACAC",
|
||||
500: "#888888",
|
||||
600: "#494949",
|
||||
700: "#3E3E3E",
|
||||
800: "#313131",
|
||||
900: "#292929",
|
||||
},
|
||||
neutral: {
|
||||
50: "#F8F8F8",
|
||||
100: "#F5F5F5",
|
||||
200: "#E1E1E1",
|
||||
300: "#CFCFCF",
|
||||
400: "#ACACAC",
|
||||
500: "#888888",
|
||||
600: "#494949",
|
||||
700: "#3E3E3E",
|
||||
800: "#313131",
|
||||
900: "#292929",
|
||||
},
|
||||
primary: {
|
||||
50: "#F4F4F4",
|
||||
100: "#E8E8E8",
|
||||
200: "#C6C6C6",
|
||||
300: "#A3A3A3",
|
||||
400: "#5F5F5F",
|
||||
500: "#1A1A1A",
|
||||
600: "#171717",
|
||||
700: "#141414",
|
||||
800: "#101010",
|
||||
900: "#0D0D0D",
|
||||
},
|
||||
secondary: {
|
||||
50: "#F5F8F7",
|
||||
100: "#EBF0F0",
|
||||
200: "#CDDAD9",
|
||||
300: "#AEC4C2",
|
||||
400: "#729894",
|
||||
500: "#356C66",
|
||||
600: "#30615C",
|
||||
700: "#28514D",
|
||||
800: "#20413D",
|
||||
900: "#223B41",
|
||||
},
|
||||
red: {
|
||||
50: "#FEF2F2",
|
||||
100: "#FEE2E2",
|
||||
200: "#FECACA",
|
||||
300: "#FCA5A5",
|
||||
400: "#F87171",
|
||||
500: "#EF4444",
|
||||
600: "#DC2626",
|
||||
700: "#B91C1C",
|
||||
800: "#991B1B",
|
||||
900: "#7F1D1D",
|
||||
},
|
||||
orange: {
|
||||
50: "#FFF7ED",
|
||||
100: "#FFEDD5",
|
||||
200: "#FED7AA",
|
||||
300: "#FDBA74",
|
||||
400: "#FB923C",
|
||||
500: "#F97316",
|
||||
600: "#EA580C",
|
||||
700: "#C2410C",
|
||||
800: "#9A3412",
|
||||
900: "#7C2D12",
|
||||
},
|
||||
green: {
|
||||
50: "#ECFDF5",
|
||||
100: "#D1FAE5",
|
||||
200: "#A7F3D0",
|
||||
300: "#6EE7B7",
|
||||
400: "#34D399",
|
||||
500: "#10B981",
|
||||
600: "#059669",
|
||||
700: "#047857",
|
||||
800: "#065F46",
|
||||
900: "#064E3B",
|
||||
},
|
||||
};
|
||||
export const All = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
{Object.keys(COLORS.brand).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.brand[color]}>
|
||||
<div className="w-full">Brand</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.brand[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.brand[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{Object.keys(COLORS.gray).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.gray[color]}>
|
||||
<div className="w-full">gray</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.gray[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.gray[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{Object.keys(COLORS.secondary).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.secondary[color]}>
|
||||
<div className="w-full">secondary</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.secondary[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.secondary[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{Object.keys(COLORS.red).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.red[color]}>
|
||||
<div className="w-full">red</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.red[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.red[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{Object.keys(COLORS.orange).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.orange[color]}>
|
||||
<div className="w-full">orange</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.orange[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.orange[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<hr />
|
||||
{Object.keys(COLORS.green).map((color) => (
|
||||
<div className="flex flex-row space-x-2" key={COLORS.green[color]}>
|
||||
<div className="w-full">green</div>
|
||||
<div className="w-full">{color}</div>
|
||||
<div className="w-full">{COLORS.green[color]}</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: COLORS.green[color],
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const ColorPickerComponent = () => {
|
||||
const [color, setColor] = useState("3B82F6");
|
||||
return <ColorPicker defaultValue={color} onChange={(val) => setColor(val)} />;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import DatePicker from "@calcom/ui/v2/booker/DatePicker";
|
||||
|
||||
export default {
|
||||
title: "Datepicker",
|
||||
component: DatePicker,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [selected, setSelected] = useState<Date | undefined>(undefined);
|
||||
return (
|
||||
<div style={{ width: "455px" }}>
|
||||
<DatePicker selected={selected} onChange={setSelected} locale="en" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Bell } from "react-feather";
|
||||
|
||||
import { EmptyScreen as EmptyScreenPattern } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "pattern/Empty Screen",
|
||||
component: EmptyScreenPattern,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof EmptyScreenPattern>;
|
||||
|
||||
export const EmptyScreenS = () => (
|
||||
<EmptyScreenPattern
|
||||
Icon={Bell}
|
||||
headline="Empty State Header"
|
||||
description="Ullamco dolor nulla sint nulla occaecat aliquip id elit fugiat et excepteur magna. Nisi tempor anim do tempor irure fugiat ad occaecat. Mollit ea eiusmod pariatur sunt deserunt eu eiusmod. Sit reprehenderit cupidatat consequat commodo in aliqua ea et. Et quis sit enim proident dolor mollit consectetur tempor dolore reprehenderit consequat adipisicing reprehenderit officia. Sint eu sunt fugiat laborum Lorem irure aute nulla et. Do non in enim ipsum ea."
|
||||
buttonText="Veniam ut ipsum"
|
||||
buttonOnClick={() => console.log("Button Clicked")}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { FormStep } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Form Step",
|
||||
component: FormStep,
|
||||
} as ComponentMeta<typeof FormStep>;
|
||||
|
||||
export const Default = () => {
|
||||
const STEPS = 4;
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-14 p-20">
|
||||
<div className="w-1/2">
|
||||
<FormStep steps={STEPS} currentStep={currentStep} />
|
||||
<div className="flex space-x-2 pt-4">
|
||||
<button onClick={() => currentStep - 1 > 0 && setCurrentStep((old) => old - 1)}>Previous</button>
|
||||
<button onClick={() => currentStep + 1 < STEPS + 1 && setCurrentStep((old) => old + 1)}>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Disabling until we figure out what is happening with the RouterMock.
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { HorizontalTabs } from "@calcom/ui/v2/navigation/tabs";
|
||||
import { HorizontalTabItemProps } from "@calcom/ui/v2/navigation/tabs/HorizontalTabItem";
|
||||
|
||||
export default {
|
||||
title: "Horizontal Tabs",
|
||||
component: HorizontalTabs,
|
||||
} as ComponentMeta<typeof HorizontalTabs>;
|
||||
|
||||
const HorizontalTabsPropsDefault: HorizontalTabItemProps[] = [
|
||||
{
|
||||
name: "Tab One",
|
||||
href: "/tab-one",
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-full p-4" style={{ backgroundColor: "#F9FAFB" }}>
|
||||
<HorizontalTabs tabs={HorizontalTabsPropsDefault} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Default.parameters = {
|
||||
nextRouter: {
|
||||
path: "/[page]",
|
||||
asPath: "/profile",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import { Copy } from "react-feather";
|
||||
|
||||
import DatePicker from "@calcom/ui/v2/form/DatePicker";
|
||||
import { TextAreaField, TextField } from "@calcom/ui/v2/form/fields";
|
||||
|
||||
export default {
|
||||
title: "Inputs",
|
||||
component: TextField,
|
||||
argTypes: {
|
||||
disabled: {
|
||||
options: [false, true],
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof TextField>;
|
||||
|
||||
const TextInputTemplate: ComponentStory<typeof TextField> = (args) => <TextField {...args} />;
|
||||
// name="demo" label="Demo Label" hint="Hint text"
|
||||
export const TextInput = TextInputTemplate.bind({});
|
||||
TextInput.args = {
|
||||
name: "demo",
|
||||
label: "Demo Label",
|
||||
hint: "Hint Text",
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const TextInputPrefix = TextInputTemplate.bind({});
|
||||
TextInputPrefix.args = {
|
||||
name: "demo",
|
||||
label: "Demo Label",
|
||||
hint: "Hint Text",
|
||||
addOnLeading: "https://",
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const TextInputSuffix = TextInputTemplate.bind({});
|
||||
TextInputSuffix.args = {
|
||||
name: "demo",
|
||||
label: "Demo Label",
|
||||
hint: "Hint Text",
|
||||
addOnSuffix: "Minutes",
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const TextInputPrefixIcon = TextInputTemplate.bind({});
|
||||
TextInputPrefixIcon.args = {
|
||||
name: "demo",
|
||||
label: "Demo Label",
|
||||
hint: "Hint Text",
|
||||
addOnFilled: false,
|
||||
addOnSuffix: <Copy />,
|
||||
disabled: false,
|
||||
};
|
||||
export const TextInputSuffixIcon = TextInputTemplate.bind({});
|
||||
TextInputSuffixIcon.args = {
|
||||
name: "demo",
|
||||
label: "Demo Label",
|
||||
hint: "Hint Text",
|
||||
addOnFilled: false,
|
||||
addOnLeading: <Copy />,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export const TextAreaInput: ComponentStory<typeof TextAreaField> = () => (
|
||||
<TextAreaField name="Text-area-input" label="Text Area" />
|
||||
);
|
||||
|
||||
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { Info } from "react-feather";
|
||||
|
||||
import { Button } from "@calcom/ui/v2";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@calcom/ui/v2/Dialog";
|
||||
import { TextField } from "@calcom/ui/v2/form/fields";
|
||||
|
||||
export default {
|
||||
title: "pattern/Modal",
|
||||
component: Dialog,
|
||||
};
|
||||
|
||||
export const Creation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => setOpen(true)}>Open Modal</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title="Header"
|
||||
description="Optional Description"
|
||||
type="creation"
|
||||
actionText="Create"
|
||||
actionOnClick={() => setOpen(false)}>
|
||||
<TextField name="Label" />
|
||||
<TextField name="Label" />
|
||||
<TextField name="Label" />
|
||||
<TextField name="Label" />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const Confirmation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Open Modal</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
title="Header"
|
||||
description="Optional Description"
|
||||
type="confirmation"
|
||||
actionText="Confirm"
|
||||
Icon={Info}
|
||||
actionOnClick={() => setOpen(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { Button } from "@calcom/ui/v2";
|
||||
import showToast from "@calcom/ui/v2/notfications";
|
||||
|
||||
export default {
|
||||
title: "Notifcations-Toasts",
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<>
|
||||
<Story />
|
||||
<Toaster />
|
||||
</>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof All>; // We have to fake this type as the story for this component isn't really a component.
|
||||
|
||||
export const All = () => (
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={() => showToast("This is a Neutral toast", "warning")}>Neutral</Button>
|
||||
<Button onClick={() => showToast("This is a Success toast", "success")}>Success</Button>
|
||||
<Button onClick={() => showToast("This is a Error toast", "error")}>Error</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Search } from "react-feather";
|
||||
|
||||
import { Button, PageHeader } from "@calcom/ui/v2";
|
||||
import { TextField } from "@calcom/ui/v2/form";
|
||||
|
||||
export default {
|
||||
title: "pattern/Page Header",
|
||||
component: PageHeader,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof PageHeader>;
|
||||
|
||||
export const Default = () => (
|
||||
<PageHeader title="Title" description="Some description about the header above" />
|
||||
);
|
||||
|
||||
export const ButtonRight = () => (
|
||||
<PageHeader
|
||||
title="Title"
|
||||
description="Some description about the header above"
|
||||
rightAlignedComponent={<Button>Button Text</Button>}
|
||||
/>
|
||||
);
|
||||
export const ComingSoon = () => (
|
||||
<PageHeader
|
||||
title="Title"
|
||||
description="Some description about the header above"
|
||||
badgeText="Coming Soon"
|
||||
badgeVariant="gray"
|
||||
/>
|
||||
);
|
||||
export const SearchInstalledApps = () => (
|
||||
<PageHeader
|
||||
title="Search booking"
|
||||
description="See upcoming and past events booking through your event type link"
|
||||
rightAlignedComponent={
|
||||
<TextField
|
||||
containerClassName="h-9 max-h-9"
|
||||
addOnLeading={<Search />}
|
||||
name="search"
|
||||
labelSrOnly
|
||||
placeholder="WIP"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as Radio from "@calcom/ui/v2/form/radio-area/Radio";
|
||||
import { RadioField } from "@calcom/ui/v2/form/radio-area/Radio";
|
||||
|
||||
export default {
|
||||
title: "Radio",
|
||||
component: RadioField,
|
||||
};
|
||||
|
||||
export const RadioGroupDemo = () => {
|
||||
return (
|
||||
<form>
|
||||
<Radio.Group aria-label="View density" defaultValue="default">
|
||||
<RadioField label="Default" id="r1" value="1" />
|
||||
<RadioField label="Next" id="r2" value="2" />
|
||||
<RadioField label="Disabled" id="r3" disabled value="1" />
|
||||
</Radio.Group>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Select } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Form/Select",
|
||||
component: Select,
|
||||
} as ComponentMeta<typeof Select>;
|
||||
|
||||
export const Single = () => <Select items={[{ value: "Test Test" }]} />;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Switch } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "Switch",
|
||||
component: Switch,
|
||||
} as ComponentMeta<typeof Switch>;
|
||||
|
||||
export const All = () => (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<p>Checked works in app but storybook doesnt like it</p>
|
||||
<Switch label="Default" />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,67 @@
|
||||
// Disabling until we figure out what is happening with the RouterMock.
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Link } from "react-feather";
|
||||
|
||||
import { VerticalTabItem } from "@calcom/ui/v2";
|
||||
|
||||
export default {
|
||||
title: "VerticalTabItem",
|
||||
component: VerticalTabItem,
|
||||
} as ComponentMeta<typeof VerticalTabItem>;
|
||||
|
||||
const TabItemProps = {
|
||||
name: "Event Setup",
|
||||
icon: Link,
|
||||
href: "/settings/event",
|
||||
info: "60 mins, Zoom",
|
||||
};
|
||||
|
||||
// export const Default = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
// export const Active = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem {...TabItemProps}></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// Mocking next router to show active state
|
||||
// Active.parameters = {
|
||||
// nextRouter: {
|
||||
// path: "/settings/[tab]",
|
||||
// asPath: "/settings/event",
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const Disabled = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem {...TabItemProps} disabled></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// export const NoIconNoInfo = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem name="Event Setup" href="/settings/event"></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// export const IconNoInfo = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem name="Event Setup" href="/settings/event" icon={Link}></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// export const IconNoInfoActive = () => (
|
||||
// <div className="h-20 w-full bg-gray-100 p-4">
|
||||
// <VerticalTabItem name="Event Setup" href="/settings/events" icon={Link}></VerticalTabItem>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
// IconNoInfoActive.paramaters = {
|
||||
// nextRouter: {
|
||||
// path: "/settings/[tab]",
|
||||
// asPath: "/settings/events",
|
||||
// },
|
||||
// };
|
||||
@@ -0,0 +1,85 @@
|
||||
// Disabling until we figure out what is happening with the RouterMock.
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Calendar, Clock, Grid, Link, RefreshCw, User, Users } from "react-feather";
|
||||
|
||||
import { VerticalTabs } from "@calcom/ui/v2/navigation/tabs";
|
||||
|
||||
export default {
|
||||
title: "Vertical Tabs",
|
||||
component: VerticalTabs,
|
||||
} as ComponentMeta<typeof VerticalTabs>;
|
||||
|
||||
const VerticalTabsPropsDefault = [
|
||||
{
|
||||
name: "Security",
|
||||
icon: Link,
|
||||
href: "/security",
|
||||
},
|
||||
{
|
||||
name: "Profile",
|
||||
icon: User,
|
||||
href: "/profile",
|
||||
},
|
||||
{
|
||||
name: "Calendar Sync",
|
||||
icon: RefreshCw,
|
||||
href: "/cal-sync",
|
||||
},
|
||||
{
|
||||
name: "Teams",
|
||||
icon: Users,
|
||||
href: "/Teams",
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-full p-4" style={{ backgroundColor: "#F9FAFB" }}>
|
||||
<VerticalTabs tabs={VerticalTabsPropsDefault} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Default.parameters = {
|
||||
nextRouter: {
|
||||
path: "/[page]",
|
||||
asPath: "/profile",
|
||||
},
|
||||
};
|
||||
|
||||
const VerticalTabsPropsInfo = [
|
||||
{
|
||||
name: "Event Setup",
|
||||
icon: Link,
|
||||
href: "/events",
|
||||
info: "60 Mins, Zoom",
|
||||
},
|
||||
{
|
||||
name: "Availability",
|
||||
icon: Calendar,
|
||||
href: "/availability",
|
||||
info: "Working Hours",
|
||||
},
|
||||
{
|
||||
name: "Limits",
|
||||
icon: Clock,
|
||||
href: "/limits",
|
||||
info: "Buffers, Limits & more...",
|
||||
},
|
||||
{
|
||||
name: "Apps",
|
||||
icon: Grid,
|
||||
href: "/apps",
|
||||
info: "3 apps - 0 active",
|
||||
},
|
||||
];
|
||||
|
||||
export const Info = () => (
|
||||
<div className="w-full p-4" style={{ backgroundColor: "#F9FAFB" }}>
|
||||
<VerticalTabs tabs={VerticalTabsPropsInfo} />
|
||||
</div>
|
||||
);
|
||||
Info.parameters = {
|
||||
nextRouter: {
|
||||
path: "/[page]",
|
||||
asPath: "/events",
|
||||
},
|
||||
};
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--brand-color: #292929;
|
||||
--brand-text-color: #ffffff;
|
||||
--brand-color-dark-mode: #fafafa;
|
||||
--brand-text-color-dark-mode: #292929;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
module.exports = {
|
||||
content: ["../../packages/ui/**/*.{js,ts,jsx,tsx}", "./stories/**/*.{js,ts,tsx,jsx}"],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
fontFamily: {
|
||||
cal: ["Cal Sans", "sans-serif"],
|
||||
mono: ["Roboto Mono", "monospace"],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
/* your primary brand color */
|
||||
brandcontrast: "var(--brand-text-color)",
|
||||
darkmodebrand: "var(--brand-color-dark-mode)",
|
||||
darkmodebrandcontrast: "var(--brand-text-color-dark-mode)",
|
||||
bookinglightest: "var(--booking-lightest-color)",
|
||||
bookinglighter: "var(--booking-lighter-color)",
|
||||
bookinglight: "var(--booking-light-color)",
|
||||
bookingmedian: "var(--booking-median-color)",
|
||||
bookingdark: "var(--booking-dark-color)",
|
||||
bookingdarker: "var(--booking-darker-color)",
|
||||
bookinghighlight: "var(--booking-highlight-color)",
|
||||
black: "#111111",
|
||||
brand: {
|
||||
// Figure out a way to automate this for self hosted users
|
||||
// Goto https://javisperez.github.io/tailwindcolorshades to generate your brand color
|
||||
50: "#d1d5db",
|
||||
100: "#9ca3af",
|
||||
200: "#6b7280",
|
||||
300: "#4b5563",
|
||||
400: "#374151",
|
||||
500: "#111827", // Brand color
|
||||
600: "#0f1623",
|
||||
700: "#0d121d",
|
||||
800: "#0a0e17",
|
||||
900: "#080c13",
|
||||
DEFAULT: "#111827",
|
||||
},
|
||||
gray: {
|
||||
50: "#F9FAFB",
|
||||
100: "#F3F4F6",
|
||||
200: "#E5E7EB",
|
||||
300: "#D1D5DB",
|
||||
400: "#9CA3AF",
|
||||
500: "#6B7280",
|
||||
600: "#4B5563",
|
||||
700: "#374151",
|
||||
800: "#1F2937",
|
||||
900: "#111827",
|
||||
},
|
||||
neutral: {
|
||||
50: "#F8F8F8",
|
||||
100: "#F5F5F5",
|
||||
200: "#E1E1E1",
|
||||
300: "#CFCFCF",
|
||||
400: "#ACACAC",
|
||||
500: "#888888",
|
||||
600: "#494949",
|
||||
700: "#3E3E3E",
|
||||
800: "#313131",
|
||||
900: "#292929",
|
||||
},
|
||||
primary: {
|
||||
50: "#F4F4F4",
|
||||
100: "#E8E8E8",
|
||||
200: "#C6C6C6",
|
||||
300: "#A3A3A3",
|
||||
400: "#5F5F5F",
|
||||
500: "#1A1A1A",
|
||||
600: "#171717",
|
||||
700: "#141414",
|
||||
800: "#101010",
|
||||
900: "#0D0D0D",
|
||||
},
|
||||
secondary: {
|
||||
50: "#F5F8F7",
|
||||
100: "#EBF0F0",
|
||||
200: "#CDDAD9",
|
||||
300: "#AEC4C2",
|
||||
400: "#729894",
|
||||
500: "#356C66",
|
||||
600: "#30615C",
|
||||
700: "#28514D",
|
||||
800: "#20413D",
|
||||
900: "#223B41",
|
||||
},
|
||||
red: {
|
||||
50: "#FFF5F5",
|
||||
100: "#FFE3E2",
|
||||
200: "#FFC9C9",
|
||||
300: "#FEA8A8",
|
||||
400: "#FF8787",
|
||||
500: "#FF6B6B",
|
||||
600: "#FA5352",
|
||||
700: "#F03E3F",
|
||||
800: "#E03130",
|
||||
900: "#C92B2B",
|
||||
},
|
||||
orange: {
|
||||
50: "#FFF4E5",
|
||||
100: "#FFE8CC",
|
||||
200: "#FFD8A8",
|
||||
300: "#FFBF78",
|
||||
400: "#FFA94E",
|
||||
500: "#FF922B",
|
||||
600: "#FD7E14",
|
||||
700: "#F76706",
|
||||
800: "#E8580C",
|
||||
900: "#D94810",
|
||||
},
|
||||
green: {
|
||||
50: "#EBFCEE",
|
||||
100: "#D2F9D9",
|
||||
200: "#B1F2BA",
|
||||
300: "#8CE99A",
|
||||
400: "#69DB7C",
|
||||
500: "#51CF66",
|
||||
600: "#40C057",
|
||||
700: "#36B24D",
|
||||
800: "#2F9E44",
|
||||
900: "#2B8A3E",
|
||||
},
|
||||
blue: {
|
||||
50: "#E7F5FF",
|
||||
100: "#D0EBFF",
|
||||
200: "#A4D8FF",
|
||||
300: "#74C0FC",
|
||||
400: "#4DABF7",
|
||||
500: "#339AF0",
|
||||
600: "#238BE6",
|
||||
700: "#1C7ED7",
|
||||
800: "#1971C2",
|
||||
900: "#1763AB",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
cal: ["inter", "sans-serif"],
|
||||
mono: ["Roboto Mono", "monospace"],
|
||||
},
|
||||
maxHeight: (theme) => ({
|
||||
0: "0",
|
||||
97: "25rem",
|
||||
...theme("spacing"),
|
||||
full: "100%",
|
||||
screen: "100vh",
|
||||
}),
|
||||
minHeight: (theme) => ({
|
||||
0: "0",
|
||||
...theme("spacing"),
|
||||
full: "100%",
|
||||
screen: "100vh",
|
||||
}),
|
||||
minWidth: (theme) => ({
|
||||
0: "0",
|
||||
...theme("spacing"),
|
||||
full: "100%",
|
||||
screen: "100vw",
|
||||
}),
|
||||
maxWidth: (theme, { breakpoints }) => ({
|
||||
0: "0",
|
||||
...theme("spacing"),
|
||||
...breakpoints(theme("screens")),
|
||||
full: "100%",
|
||||
screen: "100vw",
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@calcom/tsconfig/nextjs.json",
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,27 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "PORT=4200 next dev",
|
||||
"build": "next build",
|
||||
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
|
||||
"dev": "PORT=4200 next dev",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/swagger.json",
|
||||
"start": "PORT=4200 next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.5.1",
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "12.1.6",
|
||||
"next": "^12.2.5",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"swagger-ui-react": "4.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.9",
|
||||
"@types/react": "^18.0.9",
|
||||
"@types/react-dom": "18.0.4",
|
||||
"typescript": "4.6.4"
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
{
|
||||
"extends": "@calcom/tsconfig/nextjs.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@lib/*": ["lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"../../packages/types/*.d.ts",
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { XIcon } from "@heroicons/react/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<XIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,11 @@ import { OptionProps } from "react-select";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
interface AdditionalCalendarSelectorProps {
|
||||
isLoading?: boolean;
|
||||
@@ -45,12 +45,12 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
const options = data.items.map((item) => ({
|
||||
label: item.name,
|
||||
slug: item.slug,
|
||||
image: item.imageSrc,
|
||||
image: item.logo,
|
||||
type: item.type,
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
name={"additionalCalendar"}
|
||||
name="additionalCalendar"
|
||||
placeholder={t("connect_additional_calendar")}
|
||||
options={options}
|
||||
styles={{
|
||||
@@ -73,7 +73,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
},
|
||||
}}
|
||||
isSearchable={false}
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 font-medium text-gray-700 sm:text-sm"
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
||||
isLoading={isLoading}
|
||||
components={{ Option: ImageOption }}
|
||||
/>
|
||||
|
||||
+246
-197
@@ -1,26 +1,19 @@
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CheckIcon,
|
||||
DocumentTextIcon,
|
||||
ExternalLinkIcon,
|
||||
FlagIcon,
|
||||
MailIcon,
|
||||
PlusIcon,
|
||||
ShieldCheckIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/solid";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App as AppType } from "@calcom/types/App";
|
||||
import { Button, SkeletonButton } from "@calcom/ui";
|
||||
import Badge from "@calcom/ui/Badge";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
|
||||
import Shell from "@components/Shell";
|
||||
import Badge from "@components/ui/Badge";
|
||||
|
||||
export default function App({
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
logo,
|
||||
@@ -36,25 +29,19 @@ export default function App({
|
||||
email,
|
||||
tos,
|
||||
privacy,
|
||||
}: {
|
||||
name: string;
|
||||
type: AppType["type"];
|
||||
isGlobal?: AppType["isGlobal"];
|
||||
logo: string;
|
||||
body: React.ReactNode;
|
||||
categories: string[];
|
||||
author: string;
|
||||
pro?: boolean;
|
||||
price?: number;
|
||||
commission?: number;
|
||||
feeType?: AppType["feeType"];
|
||||
docs?: string;
|
||||
website?: string;
|
||||
email: string; // required
|
||||
tos?: string;
|
||||
privacy?: string;
|
||||
}) {
|
||||
isProOnly,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
|
||||
const mutation = useAddAppMutation(null, {
|
||||
onSuccess: () => {
|
||||
showToast("App successfully installed", "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) showToast(error.message || "App could not be installed", "error");
|
||||
},
|
||||
});
|
||||
|
||||
const priceInDollar = Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
@@ -90,176 +77,238 @@ export default function App({
|
||||
getInstalledApp(type);
|
||||
}, [type]);
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||
return (
|
||||
<>
|
||||
<Shell large isPublic>
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="bg-gray-50 px-8">
|
||||
<Link href="/apps">
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
||||
<ChevronLeftIcon className="h-5 w-5" /> {t("browse_apps")}
|
||||
</a>
|
||||
</Link>
|
||||
<div className="items-center justify-between py-4 sm:flex sm:py-8">
|
||||
<div className="flex">
|
||||
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
|
||||
<header className="px-4 py-2">
|
||||
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
||||
<h2 className="text-sm text-gray-500">
|
||||
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
||||
</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 sm:mt-0 sm:text-right">
|
||||
{!isLoading ? (
|
||||
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
|
||||
<div className="space-x-3">
|
||||
<Button StartIcon={CheckIcon} color="secondary" disabled>
|
||||
{installedAppCount > 0
|
||||
? t("active_install", { count: installedAppCount })
|
||||
: t("globally_install")}
|
||||
</Button>
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
render={(buttonProps) => (
|
||||
<Button StartIcon={PlusIcon} data-testid="install-app-button" {...buttonProps}>
|
||||
{t("add_another")}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
return (
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="bg-gray-50 px-8">
|
||||
<Link href="/apps">
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
||||
<Icon.FiChevronLeft className="h-5 w-5" /> {t("browse_apps")}
|
||||
</a>
|
||||
</Link>
|
||||
<div className="items-center justify-between py-4 sm:flex sm:py-8">
|
||||
<div className="flex">
|
||||
<img className="h-16 w-16 rounded-sm" src={logo} alt={name} />
|
||||
<header className="px-4 py-2">
|
||||
<div className="flex items-center">
|
||||
<h1 className="font-cal text-xl text-gray-900">{name}</h1>
|
||||
{isProOnly && user?.plan === "FREE" ? (
|
||||
<Badge className="ml-2" variant="default">
|
||||
PRO
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<h2 className="text-sm text-gray-500">
|
||||
<span className="capitalize">{categories[0]}</span> • {t("published_by", { author })}
|
||||
</h2>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 sm:mt-0 sm:text-right">
|
||||
{!isLoading ? (
|
||||
isGlobal || (installedAppCount > 0 && allowedMultipleInstalls) ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||
{installedAppCount > 0
|
||||
? t("active_install", { count: installedAppCount })
|
||||
: t("globally_install")}
|
||||
</Button>
|
||||
{!isGlobal && (
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
render={(buttonProps) => (
|
||||
<Button data-testid="install-app-button" {...buttonProps}>
|
||||
{t("install_app")}
|
||||
</Button>
|
||||
)}
|
||||
isProOnly={isProOnly}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
onClick: () => {
|
||||
mutation.mutate({ type });
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Button StartIcon={Icon.FiPlus} {...props} data-testid="install-app-button">
|
||||
{t("add_another")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<SkeletonButton width="24" height="10" />
|
||||
)}
|
||||
{price !== 0 && (
|
||||
<small className="block text-right">
|
||||
{feeType === "usage-based"
|
||||
? commission + "% + " + priceInDollar + "/booking"
|
||||
: priceInDollar}
|
||||
{feeType === "monthly" && "/" + t("month")}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* reintroduce once we show permissions and features
|
||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} /> */}
|
||||
</div>
|
||||
|
||||
<div className="justify-between px-8 py-10 md:flex">
|
||||
<div className="prose-sm prose mb-6">{body}</div>
|
||||
<div className="md:max-w-80 flex-1 md:ml-8">
|
||||
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
|
||||
<div className="space-x-2">
|
||||
{categories.map((category) => (
|
||||
<Link href={"/apps/categories/" + category} key={category}>
|
||||
<a>
|
||||
<Badge variant="success">{category}</Badge>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
|
||||
<small>
|
||||
{price === 0 ? (
|
||||
"Free"
|
||||
) : (
|
||||
<>
|
||||
{Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price)}
|
||||
{feeType === "monthly" && "/" + t("month")}
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
) : installedAppCount > 0 ? (
|
||||
<Button color="secondary" disabled title="App already installed">
|
||||
{t("installed")}
|
||||
</Button>
|
||||
) : (
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
isProOnly={isProOnly}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
onClick: () => {
|
||||
mutation.mutate({ type });
|
||||
},
|
||||
loading: mutation.isLoading,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Button data-testid="install-app-button" {...props}>
|
||||
{t("install_app")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<SkeletonButton width="24" height="10" />
|
||||
)}
|
||||
{price !== 0 && (
|
||||
<small className="block text-right">
|
||||
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
||||
{feeType === "monthly" && "/" + t("month")}
|
||||
</small>
|
||||
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
|
||||
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
|
||||
{docs && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={docs}>
|
||||
<BookOpenIcon className="mr-1 -mt-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{website && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={website}>
|
||||
<ExternalLinkIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<MailIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{email}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{tos && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={tos}>
|
||||
<DocumentTextIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{privacy && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<ShieldCheckIcon className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<hr className="my-6" />
|
||||
<small className="leading-1 block text-gray-500">
|
||||
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
|
||||
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
|
||||
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
|
||||
</small>
|
||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
||||
<FlagIcon className="inline h-3 w-3" /> Report App
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Shell>
|
||||
</>
|
||||
</div>
|
||||
|
||||
<div className="justify-between px-8 py-10 md:flex">
|
||||
<div className="prose-sm prose mb-6">{body}</div>
|
||||
<div className="md:max-w-80 flex-1 md:ml-8">
|
||||
<h4 className="font-medium text-gray-900 ">{t("categories")}</h4>
|
||||
<div className="space-x-2">
|
||||
{categories.map((category) => (
|
||||
<Link href={"/apps/categories/" + category} key={category}>
|
||||
<a>
|
||||
<Badge variant="success">{category}</Badge>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<h4 className="mt-8 font-medium text-gray-900 ">{t("pricing")}</h4>
|
||||
<small>
|
||||
{price === 0 ? (
|
||||
"Free"
|
||||
) : (
|
||||
<>
|
||||
{Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price)}
|
||||
{feeType === "monthly" && "/" + t("month")}
|
||||
</>
|
||||
)}
|
||||
</small>
|
||||
<h4 className="mt-8 mb-2 font-medium text-gray-900 ">{t("learn_more")}</h4>
|
||||
<ul className="prose -ml-1 -mr-1 text-xs leading-5">
|
||||
{docs && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={docs}>
|
||||
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{website && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={website}>
|
||||
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{tos && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={tos}>
|
||||
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{privacy && (
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<hr className="my-6" />
|
||||
<small className="leading-1 block text-gray-500">
|
||||
Every app published on the Cal.com App Store is open source and thoroughly tested via peer
|
||||
reviews. Nevertheless, Cal.com, Inc. does not endorse or certify these apps unless they are
|
||||
published by Cal.com. If you encounter inappropriate content or behaviour please report it.
|
||||
</small>
|
||||
<a className="mt-2 block text-xs text-red-500" href="mailto:help@cal.com">
|
||||
<Icon.FiFlag className="inline h-3 w-3" /> Report App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App(props: {
|
||||
name: string;
|
||||
type: AppType["type"];
|
||||
isGlobal?: AppType["isGlobal"];
|
||||
logo: string;
|
||||
body: React.ReactNode;
|
||||
categories: string[];
|
||||
author: string;
|
||||
pro?: boolean;
|
||||
price?: number;
|
||||
commission?: number;
|
||||
feeType?: AppType["feeType"];
|
||||
docs?: string;
|
||||
website?: string;
|
||||
email: string; // required
|
||||
tos?: string;
|
||||
privacy?: string;
|
||||
licenseRequired: AppType["licenseRequired"];
|
||||
isProOnly: AppType["isProOnly"];
|
||||
}) {
|
||||
return (
|
||||
<Shell large isPublic>
|
||||
{props.licenseRequired ? (
|
||||
<LicenseRequired>
|
||||
<Component {...props} />
|
||||
</LicenseRequired>
|
||||
) : (
|
||||
<Component {...props} />
|
||||
)}
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,9 +19,7 @@ export default function AppsShell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-12 block lg:hidden">
|
||||
{status === "authenticated" && <NavTabs tabs={tabs} linkProps={{ shallow: true }} />}
|
||||
</div>
|
||||
<div className="mb-12 block lg:hidden">{status === "authenticated" && <NavTabs tabs={tabs} />}</div>
|
||||
<main className="pb-6">{children}</main>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ const tabs = [
|
||||
export default function BookingsShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<NavTabs tabs={tabs} linkProps={{ shallow: true }} />
|
||||
<NavTabs tabs={tabs} />
|
||||
<main>{children}</main>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ import React, { useEffect, useState } from "react";
|
||||
import Select from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { trpc } from "@lib/trpc";
|
||||
import { DestinationCalendar } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
interface Props {
|
||||
onChange: (value: { externalId: string; integration: string }) => void;
|
||||
@@ -12,6 +12,7 @@ interface Props {
|
||||
hidePlaceholder?: boolean;
|
||||
/** The external Id of the connected calendar */
|
||||
value: string | undefined;
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
}
|
||||
|
||||
const DestinationCalendarSelector = ({
|
||||
@@ -19,6 +20,7 @@ const DestinationCalendarSelector = ({
|
||||
isLoading,
|
||||
value,
|
||||
hidePlaceholder,
|
||||
destinationCalendar,
|
||||
}: Props): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
@@ -61,16 +63,27 @@ const DestinationCalendarSelector = ({
|
||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
||||
key: selectedCalendar.credentialId,
|
||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
||||
options: (selectedCalendar.calendars ?? []).map((cal) => ({
|
||||
label: cal.name || "",
|
||||
value: `${cal.integration}:${cal.externalId}`,
|
||||
})),
|
||||
options: (selectedCalendar.calendars ?? [])
|
||||
.filter((cal) => cal.readOnly === false)
|
||||
.map((cal) => ({
|
||||
label: cal.name || "",
|
||||
value: `${cal.integration}:${cal.externalId}`,
|
||||
})),
|
||||
})) ?? [];
|
||||
|
||||
return (
|
||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
||||
<Select
|
||||
name={"primarySelectedCalendar"}
|
||||
placeholder={!hidePlaceholder ? `${t("select_destination_calendar")}:` : undefined}
|
||||
name="primarySelectedCalendar"
|
||||
placeholder={
|
||||
!hidePlaceholder ? (
|
||||
`${t("select_destination_calendar")}`
|
||||
) : (
|
||||
<span>
|
||||
{t("default_calendar_selected")} ({destinationCalendar?.externalId})
|
||||
</span>
|
||||
)
|
||||
}
|
||||
options={options}
|
||||
styles={{
|
||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
||||
@@ -98,7 +111,7 @@ const DestinationCalendarSelector = ({
|
||||
}}
|
||||
isSearchable={false}
|
||||
className={classNames(
|
||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 sm:text-sm",
|
||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
||||
!hidePlaceholder && "font-medium"
|
||||
)}
|
||||
onChange={(option) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ArrowLeftIcon, ChevronRightIcon, CodeIcon, EyeIcon, SunIcon } from "@heroicons/react/solid";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import classNames from "classnames";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -7,13 +6,12 @@ import { components, ControlProps } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
import { Button, Switch } from "@calcom/ui";
|
||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
||||
|
||||
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import NavTabs from "@components/NavTabs";
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
@@ -40,7 +38,7 @@ type PreviewState = {
|
||||
brandColor: string;
|
||||
};
|
||||
};
|
||||
const queryParamsForDialog = ["embedType", "tabName", "eventTypeId"];
|
||||
const queryParamsForDialog = ["embedType", "tabName", "embedUrl"];
|
||||
|
||||
const getDimension = (dimension: string) => {
|
||||
if (dimension.match(/^\d+$/)) {
|
||||
@@ -143,7 +141,7 @@ function MyComponent() {
|
||||
const cal = await getCalApi();
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
}, [])
|
||||
return <Cal calLink="${calLink}" style={{width:"${width}",height:"${height}",overflow:"scroll"}} />;
|
||||
};`;
|
||||
},
|
||||
@@ -164,7 +162,7 @@ function MyComponent() {
|
||||
Cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
}, [])
|
||||
};`;
|
||||
},
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
@@ -177,7 +175,7 @@ function MyComponent() {
|
||||
const cal = await getCalApi();
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
}, [])
|
||||
return <button data-cal-link="${calLink}" />;
|
||||
};`;
|
||||
},
|
||||
@@ -452,7 +450,7 @@ const tabs = [
|
||||
{
|
||||
name: "HTML",
|
||||
tabName: "embed-code",
|
||||
icon: CodeIcon,
|
||||
icon: Icon.FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -474,7 +472,8 @@ const tabs = [
|
||||
data-testid="embed-code"
|
||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||
name="embed-code"
|
||||
className="h-[calc(100%-50px)]"
|
||||
className="h-[calc(100%-50px)] font-mono"
|
||||
style={{ resize: "none", overflow: "auto" }}
|
||||
readOnly
|
||||
value={
|
||||
`<!-- Cal ${embedType} embed code begins -->\n` +
|
||||
@@ -488,7 +487,8 @@ ${getEmbedSnippetString()}
|
||||
${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previewState })}
|
||||
</script>
|
||||
<!-- Cal ${embedType} embed code ends -->`
|
||||
}></TextArea>
|
||||
}
|
||||
/>
|
||||
<p className="hidden text-sm text-gray-500">
|
||||
{t(
|
||||
"Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options."
|
||||
@@ -501,7 +501,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
{
|
||||
name: "React",
|
||||
tabName: "embed-react",
|
||||
icon: CodeIcon,
|
||||
icon: Icon.FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -521,8 +521,9 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
data-testid="embed-react"
|
||||
ref={ref as typeof ref & MutableRefObject<HTMLTextAreaElement>}
|
||||
name="embed-react"
|
||||
className="h-[calc(100%-50px)]"
|
||||
className="h-[calc(100%-50px)] font-mono"
|
||||
readOnly
|
||||
style={{ resize: "none", overflow: "auto" }}
|
||||
value={`/* First make sure that you have installed the package */
|
||||
|
||||
/* If you are using yarn */
|
||||
@@ -531,7 +532,8 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
/* If you are using npm */
|
||||
// npm install @calcom/embed-react
|
||||
${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, previewState })}
|
||||
`}></TextArea>
|
||||
`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}),
|
||||
@@ -539,7 +541,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
||||
{
|
||||
name: "Preview",
|
||||
tabName: "embed-preview",
|
||||
icon: EyeIcon,
|
||||
icon: Icon.FiEye,
|
||||
type: "iframe",
|
||||
Component: forwardRef<
|
||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||
@@ -575,7 +577,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||
return (
|
||||
<components.Control {...props}>
|
||||
<SunIcon className="h-[32px] w-[32px] text-gray-500" />
|
||||
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||
{children}
|
||||
</components.Control>
|
||||
);
|
||||
@@ -594,10 +596,10 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
<p className="text-sm text-gray-500">{t("choose_ways_put_cal_site")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex items-start">
|
||||
{embeds.map((embed, index) => (
|
||||
<button
|
||||
className="mr-2 w-1/3 p-3 text-left hover:rounded-md hover:border hover:bg-neutral-100"
|
||||
className="mr-2 w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100"
|
||||
key={index}
|
||||
data-testid={embed.type}
|
||||
onClick={() => {
|
||||
@@ -621,11 +623,11 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
};
|
||||
|
||||
const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
eventTypeId,
|
||||
embedType,
|
||||
embedUrl,
|
||||
}: {
|
||||
eventTypeId: EventType["id"];
|
||||
embedType: EmbedType;
|
||||
embedUrl: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -640,13 +642,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
const refOfEmbedCodesRefs = useRef(embedCodeRefs);
|
||||
const embed = embeds.find((embed) => embed.type === embedType);
|
||||
|
||||
const { data: eventType, isLoading } = trpc.useQuery([
|
||||
"viewer.eventTypes.get",
|
||||
{
|
||||
id: +eventTypeId,
|
||||
},
|
||||
]);
|
||||
|
||||
const [isEmbedCustomizationOpen, setIsEmbedCustomizationOpen] = useState(true);
|
||||
const [isBookingCustomizationOpen, setIsBookingCustomizationOpen] = useState(true);
|
||||
const [previewState, setPreviewState] = useState({
|
||||
@@ -688,18 +683,12 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!embed || !eventType) {
|
||||
if (!embed || !embedUrl) {
|
||||
close();
|
||||
return null;
|
||||
}
|
||||
|
||||
const calLink = `${eventType.team ? `team/${eventType.team.slug}` : eventType.users[0].username}/${
|
||||
eventType.slug
|
||||
}`;
|
||||
const calLink = decodeURIComponent(embedUrl);
|
||||
|
||||
const addToPalette = (update: typeof previewState["palette"]) => {
|
||||
setPreviewState((previewState) => {
|
||||
@@ -802,11 +791,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
},
|
||||
});
|
||||
}}>
|
||||
<ArrowLeftIcon className="mr-4 w-4"></ArrowLeftIcon>
|
||||
<Icon.FiArrowLeft className="mr-4 w-4" />
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")}></hr>
|
||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
@@ -821,7 +810,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
? "Floating Popup Customization"
|
||||
: "Element Click Customization"}
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
@@ -852,7 +841,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
}}
|
||||
addOnLeading={<InputLeading>W</InputLeading>}
|
||||
/>
|
||||
<span className="p-2">x</span>
|
||||
<span className="p-2">×</span>
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
name="height"
|
||||
@@ -918,7 +907,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
}}></Switch>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -939,7 +929,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
});
|
||||
}}
|
||||
defaultValue={FloatingPopupPositionOptions[0]}
|
||||
options={FloatingPopupPositionOptions}></Select>
|
||||
options={FloatingPopupPositionOptions}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -960,7 +951,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
}}></ColorPicker>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -982,20 +974,21 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
}}></ColorPicker>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<hr className="mt-4"></hr>
|
||||
<hr className="mt-4" />
|
||||
<div className="mt-4 font-medium">
|
||||
<Collapsible
|
||||
open={isBookingCustomizationOpen}
|
||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger className="flex w-full" type="button">
|
||||
<div className="text-base font-medium text-neutral-900">Cal Booking Customization</div>
|
||||
<ChevronRightIcon
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
@@ -1022,7 +1015,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={ThemeOptions}></Select>
|
||||
options={ThemeOptions}
|
||||
/>
|
||||
</Label>
|
||||
{[
|
||||
{ name: "brandColor", title: "Brand Color" },
|
||||
@@ -1065,7 +1059,8 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
embedType={embedType}
|
||||
calLink={calLink}
|
||||
previewState={previewState}
|
||||
ref={refOfEmbedCodesRefs.current[tab.name]}></tab.Component>
|
||||
ref={refOfEmbedCodesRefs.current[tab.name]}
|
||||
/>
|
||||
) : (
|
||||
<tab.Component
|
||||
embedType={embedType}
|
||||
@@ -1075,7 +1070,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"}></div>
|
||||
<div className={router.query.tabName == "embed-preview" ? "block" : "hidden"} />
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
{tab.type === "code" ? (
|
||||
@@ -1107,39 +1102,46 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
|
||||
export const EmbedDialog = () => {
|
||||
const router = useRouter();
|
||||
const eventTypeId: EventType["id"] = +(router.query.eventTypeId as string);
|
||||
const embedUrl: string = router.query.embedUrl as string;
|
||||
return (
|
||||
<Dialog name="embed" clearQueryParamsOnClose={queryParamsForDialog}>
|
||||
<Dialog
|
||||
name="embed"
|
||||
clearQueryParamsOnClose={queryParamsForDialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) window.resetEmbedStatus();
|
||||
}}>
|
||||
{!router.query.embedType ? (
|
||||
<ChooseEmbedTypesDialogContent />
|
||||
) : (
|
||||
<EmbedTypeCodeAndPreviewDialogContent
|
||||
eventTypeId={eventTypeId}
|
||||
embedType={router.query.embedType as EmbedType}
|
||||
embedUrl={embedUrl}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmbedButton = ({
|
||||
eventTypeId,
|
||||
className = "",
|
||||
dark,
|
||||
...props
|
||||
}: {
|
||||
eventTypeId: EventType["id"];
|
||||
type EmbedButtonProps<T> = {
|
||||
embedUrl: string;
|
||||
children?: React.ReactNode;
|
||||
className: string;
|
||||
dark?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
as?: T;
|
||||
};
|
||||
|
||||
export const EmbedButton = <T extends React.ElementType>({
|
||||
embedUrl,
|
||||
children,
|
||||
className = "",
|
||||
as,
|
||||
...props
|
||||
}: EmbedButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
|
||||
const router = useRouter();
|
||||
className = classNames(className, "hidden lg:flex");
|
||||
const openEmbedModal = () => {
|
||||
const query = {
|
||||
...router.query,
|
||||
dialog: "embed",
|
||||
eventTypeId,
|
||||
embedUrl,
|
||||
};
|
||||
router.push(
|
||||
{
|
||||
@@ -1150,20 +1152,16 @@ export const EmbedButton = ({
|
||||
{ shallow: true }
|
||||
);
|
||||
};
|
||||
const Component = as ?? Button;
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="sm"
|
||||
className={className}
|
||||
<Component
|
||||
{...props}
|
||||
data-test-eventtype-id={eventTypeId}
|
||||
data-testid={"event-type-embed"}
|
||||
className={className}
|
||||
data-test-embed-url={embedUrl}
|
||||
data-testid="embed"
|
||||
onClick={() => openEmbedModal()}>
|
||||
<CodeIcon
|
||||
className={classNames("h-4 w-4 ltr:mr-2 rtl:ml-2", dark ? "" : "text-neutral-500")}></CodeIcon>
|
||||
{t("Embed")}
|
||||
</Button>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { trpc } from "@lib/trpc";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n() {
|
||||
return trpc.useQuery(["viewer.public.i18n"], {
|
||||
staleTime: Infinity,
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
* We intend to not cache i18n query
|
||||
**/
|
||||
context: { skipBatch: true },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function ImageUploader({
|
||||
</div>
|
||||
)}
|
||||
{result && <CropContainer imageSrc={result as string} onCropComplete={setCroppedAreaPixels} />}
|
||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 hover:shadow-sm focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
||||
<label className="mt-8 rounded-sm border border-gray-300 bg-white px-3 py-1 text-xs font-medium leading-4 text-gray-700 hover:bg-gray-50 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-gray-800 dark:bg-transparent dark:text-white dark:hover:bg-gray-900">
|
||||
<input
|
||||
onInput={onInputFile}
|
||||
type="file"
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface NavTabProps {
|
||||
tabName?: string;
|
||||
icon?: SVGComponent;
|
||||
adminRequired?: boolean;
|
||||
className?: string;
|
||||
}[];
|
||||
linkProps?: Omit<LinkProps, "href">;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
: noop;
|
||||
|
||||
const Component = tab.adminRequired ? AdminRequired : Fragment;
|
||||
|
||||
const className = tab.className || "";
|
||||
return (
|
||||
<Component key={tab.name}>
|
||||
<Link key={tab.name} href={href} {...linkProps}>
|
||||
@@ -68,14 +69,15 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
isCurrent
|
||||
? "border-neutral-900 text-neutral-900"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700",
|
||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium"
|
||||
"group inline-flex items-center border-b-2 py-4 px-1 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
aria-current={isCurrent ? "page" : undefined}>
|
||||
{tab.icon && (
|
||||
<tab.icon
|
||||
className={classNames(
|
||||
isCurrent ? "text-neutral-900" : "text-gray-400 group-hover:text-gray-500",
|
||||
"-ml-0.5 hidden h-5 w-5 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||
"-ml-0.5 hidden h-4 w-4 ltr:mr-2 rtl:ml-2 sm:inline-block"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
@@ -1,48 +1,41 @@
|
||||
import {
|
||||
CreditCardIcon,
|
||||
KeyIcon,
|
||||
LockClosedIcon,
|
||||
UserGroupIcon,
|
||||
UserIcon,
|
||||
CodeIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import ErrorBoundary from "@lib/ErrorBoundary";
|
||||
import ErrorBoundary from "@calcom/ui/ErrorBoundary";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
import Shell from "./Shell";
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "profile",
|
||||
href: "/settings/profile",
|
||||
icon: UserIcon,
|
||||
icon: Icon.FiUser,
|
||||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
icon: UserGroupIcon,
|
||||
icon: Icon.FiUsers,
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
href: "/settings/security",
|
||||
icon: KeyIcon,
|
||||
icon: Icon.FiKey,
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
href: "/settings/developer",
|
||||
icon: CodeIcon,
|
||||
icon: Icon.FiTerminal,
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: CreditCardIcon,
|
||||
icon: Icon.FiCreditCard,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
href: "/settings/admin",
|
||||
icon: LockClosedIcon,
|
||||
icon: Icon.FiLock,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { InformationCircleIcon } from "@heroicons/react/outline";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
export function UpgradeToProDialog({
|
||||
modalOpen,
|
||||
@@ -20,7 +19,7 @@ export function UpgradeToProDialog({
|
||||
<Dialog open={modalOpen}>
|
||||
<DialogContent>
|
||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100">
|
||||
<InformationCircleIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />
|
||||
<Icon.FiAlertTriangle className="h-6 w-6 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="mt-3 sm:mt-0 sm:text-left">
|
||||
@@ -43,7 +42,7 @@ export function UpgradeToProDialog({
|
||||
</div>
|
||||
<div className="mt-5 gap-x-2 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<DialogClose asChild>
|
||||
<Button className="btn-wide table-cell text-center" onClick={() => setModalOpen(false)}>
|
||||
<Button className="table-cell w-full text-center" onClick={() => setModalOpen(false)}>
|
||||
{t("dismiss")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function AllApps({ apps }: { apps: App[] }) {
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
isProOnly={app.isProOnly}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import Badge from "@calcom/ui/Badge";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
interface AppCardProps {
|
||||
@@ -10,9 +12,11 @@ interface AppCardProps {
|
||||
description: string;
|
||||
rating: number;
|
||||
reviews?: number;
|
||||
isProOnly?: boolean;
|
||||
}
|
||||
|
||||
export default function AppCard(props: AppCardProps) {
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
return (
|
||||
<Link href={"/apps/" + props.slug}>
|
||||
<a
|
||||
@@ -32,7 +36,14 @@ export default function AppCard(props: AppCardProps) {
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<h3 className="font-medium">{props.name}</h3>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium">{props.name}</h3>
|
||||
{props.isProOnly && user?.plan === "FREE" ? (
|
||||
<Badge className="ml-2" variant="default">
|
||||
PRO
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Glide, { Options } from "@glidejs/glide";
|
||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
||||
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
const Slider = <T extends string | unknown>({
|
||||
title = "",
|
||||
className = "",
|
||||
@@ -51,10 +52,10 @@ const Slider = <T extends string | unknown>({
|
||||
)}
|
||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
||||
<button data-glide-dir="<" className="mr-4">
|
||||
<ArrowLeftIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
<button data-glide-dir=">">
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||
logo={app.logo}
|
||||
rating={app.rating}
|
||||
reviews={app.reviews}
|
||||
isProOnly={app.isProOnly}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,11 @@ import { signIn } from "next-auth/react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
@@ -34,7 +34,7 @@ export default function SAMLLogin(props: Props) {
|
||||
<div className="mt-5">
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid={"saml"}
|
||||
data-testid="saml"
|
||||
className="flex w-full justify-center"
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { PlusIcon } from "@heroicons/react/solid";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
|
||||
const router = useRouter();
|
||||
@@ -41,7 +41,7 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
||||
return (
|
||||
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid={name} StartIcon={PlusIcon}>
|
||||
<Button data-testid={name} StartIcon={Icon.FiPlus}>
|
||||
{t("new_schedule_btn")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -65,7 +65,7 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm shadow-sm"
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
placeholder={t("default_schedule_name")}
|
||||
{...register("name")}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import { DuplicateIcon } from "@heroicons/react/solid";
|
||||
import classNames from "classnames";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
@@ -8,7 +6,9 @@ import { GroupBase, Props } from "react-select";
|
||||
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown";
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
|
||||
import { defaultDayRange } from "@lib/availability";
|
||||
import { weekdayNames } from "@lib/core/i18n/weekday";
|
||||
@@ -202,7 +202,7 @@ export const DayRanges = ({
|
||||
const { setValue, watch } = useFormContext();
|
||||
// XXX: Hack to make copying times work; `fields` is out of date until save.
|
||||
const watcher = watch(name);
|
||||
|
||||
const { t } = useLocale();
|
||||
const { fields, replace, append, remove } = useFieldArray({
|
||||
name,
|
||||
});
|
||||
@@ -230,36 +230,40 @@ export const DayRanges = ({
|
||||
<div className="space-y-2">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center rtl:space-x-reverse">
|
||||
<div className="flex flex-grow sm:flex-grow-0">
|
||||
<div className="flex flex-grow space-x-1 sm:flex-grow-0">
|
||||
<TimeRangeField name={`${name}.${index}`} />
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="minimal"
|
||||
StartIcon={TrashIcon}
|
||||
type="button"
|
||||
StartIcon={Icon.FiTrash}
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
{index === 0 && (
|
||||
<div className="absolute top-2 right-0 text-right sm:relative sm:top-0 sm:flex-grow">
|
||||
<Button
|
||||
className="text-neutral-400"
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={PlusIcon}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
<Tooltip content={t("add_time_availability") as string}>
|
||||
<Button
|
||||
className="text-neutral-400"
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiPlus}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={DuplicateIcon}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<Tooltip content={t("duplicate") as string}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="icon"
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={handleAppend}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent>
|
||||
<CopyTimes
|
||||
disabled={[parseInt(name.substring(name.lastIndexOf(".") + 1), 10)]}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Availability } from "@calcom/prisma/client";
|
||||
import { inferQueryOutput } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
|
||||
|
||||
import { inferQueryOutput } from "@lib/trpc";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
export function ScheduleListItem({
|
||||
schedule,
|
||||
@@ -48,7 +47,7 @@ export function ScheduleListItem({
|
||||
</div>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
|
||||
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
|
||||
<Icon.FiMoreHorizontal className="h-5 w-5 group-hover:text-gray-800" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
@@ -62,7 +61,7 @@ export function ScheduleListItem({
|
||||
type="button"
|
||||
color="warn"
|
||||
className="w-full font-normal"
|
||||
StartIcon={isDeleting ? undefined : TrashIcon}
|
||||
StartIcon={isDeleting ? undefined : Icon.FiTrash}
|
||||
loading={isDeleting}>
|
||||
{isDeleting ? t("deleting") : t("delete_schedule")}
|
||||
</Button>
|
||||
|
||||
@@ -19,13 +19,13 @@ function SkeletonItem() {
|
||||
<li className="group flex w-full items-center justify-between px-2 py-[23px] sm:px-6">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText width="32" height="4"></SkeletonText>
|
||||
<SkeletonText width="32" height="2"></SkeletonText>
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText width="32" height="2" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 lg:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
<SkeletonText width="12" height="6"></SkeletonText>
|
||||
<SkeletonText width="12" height="6" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -37,8 +37,8 @@ export const AvailabilitySelectSkeletonLoader = () => {
|
||||
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText width="32" height="4"></SkeletonText>
|
||||
<SkeletonText width="4" height="4"></SkeletonText>
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText width="4" height="4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
@@ -6,23 +5,18 @@ import { FC, useEffect, useState } from "react";
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { timeZone } from "@lib/clock";
|
||||
|
||||
import type { Slot } from "@server/routers/viewer/slots";
|
||||
|
||||
type AvailableTimesProps = {
|
||||
timeFormat: string;
|
||||
eventTypeId: number;
|
||||
recurringCount: number | undefined;
|
||||
eventTypeSlug: string;
|
||||
date: Dayjs;
|
||||
users: {
|
||||
username: string | null;
|
||||
}[];
|
||||
schedulingType: SchedulingType | null;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
slots?: Slot[];
|
||||
isLoading: boolean;
|
||||
@@ -36,7 +30,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
eventTypeSlug,
|
||||
recurringCount,
|
||||
timeFormat,
|
||||
schedulingType,
|
||||
seatsPerTimeSlot,
|
||||
}) => {
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -50,14 +43,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mt-8 flex flex-col text-center sm:mt-0 sm:w-1/3 sm:pl-4 md:-mb-5">
|
||||
<div className="mb-4 text-left text-lg font-light text-gray-600">
|
||||
<span className="text-bookingdarker w-1/2 dark:text-white">
|
||||
<strong>{nameOfDay(i18n.language, Number(date.format("d")))}</strong>
|
||||
<span className="text-bookinglight">
|
||||
{date.format(", D ")}
|
||||
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
|
||||
</span>
|
||||
<div className="mt-8 flex flex-col px-4 text-center sm:mt-0 sm:w-1/3 sm:p-5 md:-mb-4 ">
|
||||
<div className="mb-4 text-left text-base">
|
||||
<span className="text-bookingdarker mb-8 w-1/2 break-words font-semibold text-gray-900 dark:text-white">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")))}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
{date.format(", D ")}
|
||||
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid flex-grow grid-cols-2 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||
@@ -83,10 +76,6 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
bookingUrl.query.rescheduleUid = rescheduleUid as string;
|
||||
}
|
||||
|
||||
if (schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
bookingUrl.query.user = slot.users;
|
||||
}
|
||||
|
||||
// If event already has an attendee add booking id
|
||||
if (slot.bookingUid) {
|
||||
bookingUrl.query.bookingUid = slot.bookingUid;
|
||||
@@ -102,13 +91,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}>
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && <p className={`text-sm`}>{t("booking_full")}</p>}
|
||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={bookingUrl} prefetch={false}>
|
||||
<a
|
||||
className={classNames(
|
||||
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
"text-primary-500 hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 block rounded-sm border bg-white py-4 text-sm font-medium hover:text-white dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black",
|
||||
brand === "#fff" || brand === "#ffffff" ? "border-brandcontrast" : "border-brand"
|
||||
)}
|
||||
data-testid="time">
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import {
|
||||
BanIcon,
|
||||
CheckIcon,
|
||||
ClockIcon,
|
||||
LocationMarkerIcon,
|
||||
PaperAirplaneIcon,
|
||||
PencilAltIcon,
|
||||
XIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
import { RefreshIcon } from "@heroicons/react/solid";
|
||||
import { BookingStatus } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
import { TextArea } from "@calcom/ui/form/fields";
|
||||
|
||||
@@ -28,8 +19,7 @@ import { HttpError } from "@lib/core/http/error";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import { linkValueToString } from "@lib/linkValueToString";
|
||||
import { LocationType } from "@lib/location";
|
||||
import { parseRecurringDates } from "@lib/parseDate";
|
||||
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { extractRecurringDates } from "@lib/parseDate";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
@@ -41,7 +31,7 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
|
||||
type BookingItemProps = BookingItem & {
|
||||
listingStatus: BookingListingStatus;
|
||||
recurringCount?: number;
|
||||
recurringBookings?: BookingItem[];
|
||||
};
|
||||
|
||||
function BookingListItem(booking: BookingItemProps) {
|
||||
@@ -102,7 +92,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: BanIcon,
|
||||
icon: Icon.FiSlash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
{
|
||||
@@ -114,7 +104,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
mutation.mutate(true);
|
||||
},
|
||||
icon: CheckIcon,
|
||||
icon: Icon.FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
color: "primary",
|
||||
},
|
||||
@@ -127,24 +117,30 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
booking.listingStatus === "recurring" && booking.recurringEventId !== null
|
||||
? t("cancel_all_remaining")
|
||||
: t("cancel"),
|
||||
href: `/cancel/${booking.uid}`,
|
||||
icon: XIcon,
|
||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||
cancel all remaining bookings or just that booking instance. */
|
||||
href: `/cancel/${booking.uid}${
|
||||
booking.listingStatus === "recurring" && booking.recurringEventId !== null
|
||||
? "?allRemainingBookings=true"
|
||||
: ""
|
||||
}`,
|
||||
icon: Icon.FiX,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
label: t("edit_booking"),
|
||||
icon: PencilAltIcon,
|
||||
icon: Icon.FiEdit,
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: ClockIcon,
|
||||
icon: Icon.FiClock,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: PaperAirplaneIcon,
|
||||
iconClassName: "rotate-45 w-[18px] -ml-[2px]",
|
||||
icon: Icon.FiSend,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
setIsOpenRescheduleDialog(true);
|
||||
@@ -156,7 +152,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: LocationMarkerIcon,
|
||||
icon: Icon.FiMapPin,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -169,7 +165,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
<PaperAirplaneIcon className="-mt-[1px] w-4 rotate-45" />
|
||||
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -191,7 +187,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
if (
|
||||
newLocationType === LocationType.InPerson ||
|
||||
newLocationType === LocationType.Link ||
|
||||
newLocationType === LocationType.UserPhone
|
||||
newLocationType === LocationType.UserPhone ||
|
||||
newLocationType === LocationType.Riverside ||
|
||||
newLocationType === LocationType.Around ||
|
||||
newLocationType === LocationType.Whereby ||
|
||||
newLocationType === LocationType.Ping
|
||||
) {
|
||||
newLocation = details[Object.keys(details)[0]];
|
||||
}
|
||||
@@ -202,14 +202,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
let recurringStrings: string[] = [];
|
||||
let recurringDates: Date[] = [];
|
||||
const today = new Date();
|
||||
if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||
{
|
||||
startDate: booking.startTime,
|
||||
timeZone: user?.timeZone,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent),
|
||||
recurringCount: booking.recurringCount,
|
||||
},
|
||||
if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||
[recurringStrings, recurringDates] = extractRecurringDates(
|
||||
booking.recurringBookings,
|
||||
user?.timeZone,
|
||||
i18n
|
||||
);
|
||||
if (booking.status === BookingStatus.PENDING) {
|
||||
@@ -308,7 +304,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{booking.recurringCount &&
|
||||
{booking.recurringBookings &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
|
||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||
@@ -318,8 +314,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<p key={key}>{aDate}</p>
|
||||
))}>
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="float-left mr-1 mt-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="pl-[21px]">
|
||||
<Icon.FiRefreshCcw
|
||||
stroke-width="3"
|
||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
||||
/>
|
||||
<p className="mt-1 pl-5 text-xs">
|
||||
{booking.status === BookingStatus.ACCEPTED
|
||||
? `${t("event_remaining", {
|
||||
count: recurringDates.length,
|
||||
@@ -327,7 +326,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
: getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: booking.eventType.recurringEvent,
|
||||
recurringCount: booking.recurringCount,
|
||||
recurringCount: booking.recurringBookings.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -32,90 +31,90 @@ export default function CancelBooking(props: Props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
const { isReady, Theme } = useTheme(props.theme);
|
||||
useTheme(props.theme);
|
||||
|
||||
if (isReady) {
|
||||
return (
|
||||
<>
|
||||
<Theme />
|
||||
{error && (
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<XIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
{error && (
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
)}
|
||||
{!error && (
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
||||
<textarea
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex rtl:space-x-reverse">
|
||||
{!props.recurringEvent && (
|
||||
<div className="w-full">
|
||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full space-x-2 text-right">
|
||||
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{error}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!error && (
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
||||
<textarea
|
||||
placeholder={t("cancellation_reason_placeholder")}
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
||||
rows={3}
|
||||
/>
|
||||
<div className=" flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
|
||||
{!props.recurringEvent && (
|
||||
<div className=" border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
uid: booking?.uid,
|
||||
reason: cancellationReason,
|
||||
};
|
||||
|
||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
await router.push(
|
||||
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
|
||||
profile.slug
|
||||
}&team=${team ? 1 : 0}&recurring=${!!props.recurringEvent}`
|
||||
);
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError(
|
||||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
|
||||
"please_try_again"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("cancel_event")}
|
||||
color="secondary"
|
||||
className="border-0 sm:border"
|
||||
onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-2 flex w-full space-x-2 text-right sm:mb-0 ">
|
||||
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex grow justify-center"
|
||||
data-testid="cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
uid: booking?.uid,
|
||||
reason: cancellationReason,
|
||||
};
|
||||
|
||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
await router.push(
|
||||
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
|
||||
profile.slug
|
||||
}&team=${team ? 1 : 0}&recurring=${!!props.recurringEvent}`
|
||||
);
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError(
|
||||
`${t("error_with_status_code_occured", { status: res.status })} ${t(
|
||||
"please_try_again"
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("cancel_event")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Switch from "@calcom/ui/Switch";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import TimezoneSelect, { ITimezoneOption } from "@components/ui/form/TimezoneSelect";
|
||||
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/form/TimezoneSelect";
|
||||
|
||||
import { is24h, timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
onSelectTimeZone: (selectedTimeZone: string) => void;
|
||||
onToggle24hClock: (is24hClock: boolean) => void;
|
||||
timeFormat: string;
|
||||
};
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
|
||||
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const [is24hClock, setIs24hClock] = useState(false);
|
||||
const [is24hClock, setIs24hClock] = useState(timeFormat === "HH:mm" && true);
|
||||
const { t } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,9 +33,9 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
|
||||
};
|
||||
|
||||
return selectedTimeZone !== "" ? (
|
||||
<div className="max-w-80 absolute z-10 w-full rounded-sm border border-gray-200 bg-white px-4 py-2 dark:border-0 dark:bg-gray-700">
|
||||
<div className="max-w-80 absolute z-10 w-full rounded-sm rounded border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm dark:border-0 dark:bg-gray-700">
|
||||
<div className="mb-4 flex">
|
||||
<div className="font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<label className="ltl:mr-3 mr-2 align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
|
||||
{t("am_pm")}
|
||||
@@ -53,7 +52,7 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone }) => {
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 shadow-sm focus:ring-black sm:text-sm"
|
||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
|
||||
import AvatarGroup, { AvatarGroupProps } from "@components/ui/AvatarGroup";
|
||||
|
||||
export const UserAvatars = ({
|
||||
profile,
|
||||
users,
|
||||
...props
|
||||
}: {
|
||||
profile: { image: string | null; name?: string | null };
|
||||
showMembers: boolean;
|
||||
users: { username: string | null; name?: string | null }[];
|
||||
} & Pick<AvatarGroupProps, "size" | "truncateAfter">) => {
|
||||
const showMembers = !users.find((user) => user.name === profile.name) && props.showMembers;
|
||||
return (
|
||||
<AvatarGroup
|
||||
border="border-2 dark:border-gray-800 border-white"
|
||||
items={
|
||||
[
|
||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
||||
...(showMembers
|
||||
? users.map((user) => ({
|
||||
title: user.name,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
alt: user.name || undefined,
|
||||
}))
|
||||
: []),
|
||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||
}
|
||||
size={props.size}
|
||||
truncateAfter={props.truncateAfter}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,8 @@
|
||||
// Get router variables
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
ClipboardCheckIcon,
|
||||
ClockIcon,
|
||||
CreditCardIcon,
|
||||
GlobeIcon,
|
||||
InformationCircleIcon,
|
||||
LocationMarkerIcon,
|
||||
RefreshIcon,
|
||||
VideoCameraIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { EventType } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useContracts } from "contexts/contractsContext";
|
||||
import { TFunction } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
@@ -30,27 +16,31 @@ import {
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { useContracts } from "@calcom/features/ee/web3/contexts/contractsContext";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import DatePicker from "@calcom/ui/booker/DatePicker";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
// import { timeZone } from "@lib/clock";
|
||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import CustomBranding from "@components/CustomBranding";
|
||||
import AvailableTimes from "@components/booking/AvailableTimes";
|
||||
import TimeOptions from "@components/booking/TimeOptions";
|
||||
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import PoweredByCal from "@components/ui/PoweredByCal";
|
||||
|
||||
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
||||
@@ -64,6 +54,10 @@ export const locationKeyToString = (location: LocationObject, t: TFunction) => {
|
||||
case LocationType.InPerson:
|
||||
return location.address || "In Person"; // If disabled address won't exist on the object
|
||||
case LocationType.Link:
|
||||
case LocationType.Ping:
|
||||
case LocationType.Riverside:
|
||||
case LocationType.Around:
|
||||
case LocationType.Whereby:
|
||||
return location.link || "Link"; // If disabled link won't exist on the object
|
||||
case LocationType.Phone:
|
||||
return t("your_number");
|
||||
@@ -97,7 +91,7 @@ const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
||||
return (
|
||||
<div className="flex h-full flex-col justify-end">
|
||||
<button title={t("profile")} onClick={() => router.replace(`${WEBSITE_URL}/${slug}`)}>
|
||||
<ArrowLeftIcon className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white" />
|
||||
<Icon.FiArrowLeft className="h-4 w-4 text-black transition-opacity hover:cursor-pointer dark:text-white" />
|
||||
<p className="sr-only">Go Back</p>
|
||||
</button>
|
||||
</div>
|
||||
@@ -106,20 +100,26 @@ const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
||||
|
||||
const useSlots = ({
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
startTime,
|
||||
endTime,
|
||||
usernameList,
|
||||
timeZone,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
eventTypeSlug: string;
|
||||
startTime?: Dayjs;
|
||||
endTime?: Dayjs;
|
||||
timeZone: string;
|
||||
usernameList: string[];
|
||||
timeZone?: string;
|
||||
}) => {
|
||||
const { data, isLoading, isIdle } = trpc.useQuery(
|
||||
[
|
||||
"viewer.public.slots.getSchedule",
|
||||
{
|
||||
eventTypeId,
|
||||
eventTypeSlug,
|
||||
usernameList,
|
||||
startTime: startTime?.toISOString() || "",
|
||||
endTime: endTime?.toISOString() || "",
|
||||
timeZone,
|
||||
@@ -127,7 +127,6 @@ const useSlots = ({
|
||||
],
|
||||
{ enabled: !!startTime && !!endTime }
|
||||
);
|
||||
|
||||
const [cachedSlots, setCachedSlots] = useState<NonNullable<typeof data>["slots"]>({});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -145,14 +144,16 @@ const SlotPicker = ({
|
||||
timeFormat,
|
||||
timeZone,
|
||||
recurringEventCount,
|
||||
users,
|
||||
seatsPerTimeSlot,
|
||||
weekStart = 0,
|
||||
}: {
|
||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
||||
timeFormat: string;
|
||||
timeZone: string;
|
||||
timeZone?: string;
|
||||
seatsPerTimeSlot?: number;
|
||||
recurringEventCount?: number;
|
||||
users: string[];
|
||||
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||
@@ -166,14 +167,18 @@ const SlotPicker = ({
|
||||
|
||||
// Etc/GMT is not actually a timeZone, so handle this select option explicitly to prevent a hard crash.
|
||||
if (timeZone === "Etc/GMT") {
|
||||
setBrowsingDate(dayjs.utc(month).startOf("month"));
|
||||
setBrowsingDate(dayjs.utc(month).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0));
|
||||
if (date) {
|
||||
setSelectedDate(dayjs.utc(date));
|
||||
}
|
||||
} else {
|
||||
setBrowsingDate(dayjs(month).tz(timeZone, true).startOf("month"));
|
||||
// Set the start of the month without shifting time like startOf() may do.
|
||||
setBrowsingDate(
|
||||
dayjs.tz(month, timeZone).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0)
|
||||
);
|
||||
if (date) {
|
||||
setSelectedDate(dayjs(date).tz(timeZone, true));
|
||||
// It's important to set the date immediately to the timeZone, dayjs(date) will convert to browsertime.
|
||||
setSelectedDate(dayjs.tz(date, timeZone));
|
||||
}
|
||||
}
|
||||
}, [router.isReady, month, date, timeZone]);
|
||||
@@ -181,12 +186,16 @@ const SlotPicker = ({
|
||||
const { i18n, isLocaleReady } = useLocale();
|
||||
const { slots: _1 } = useSlots({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: selectedDate?.startOf("day"),
|
||||
endTime: selectedDate?.endOf("day"),
|
||||
timeZone,
|
||||
});
|
||||
const { slots: _2, isLoading } = useSlots({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: browsingDate?.startOf("month"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
@@ -198,12 +207,12 @@ const SlotPicker = ({
|
||||
<>
|
||||
<DatePicker
|
||||
isLoading={isLoading}
|
||||
className={
|
||||
"mt-8 w-full sm:mt-0 sm:min-w-[455px] " +
|
||||
(selectedDate
|
||||
? "sm:w-1/2 sm:border-r sm:pl-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
|
||||
: "sm:pl-4")
|
||||
}
|
||||
className={classNames(
|
||||
"mt-8 w-full sm:mt-0 sm:min-w-[455px]",
|
||||
selectedDate
|
||||
? "border-gray-200 sm:w-1/2 sm:border-r sm:p-4 sm:pr-6 sm:dark:border-gray-700 md:w-1/3 "
|
||||
: "sm:p-4"
|
||||
)}
|
||||
includedDates={Object.keys(slots).filter((k) => slots[k].length > 0)}
|
||||
locale={isLocaleReady ? i18n.language : "en"}
|
||||
selected={selectedDate}
|
||||
@@ -227,8 +236,6 @@ const SlotPicker = ({
|
||||
eventTypeSlug={eventType.slug}
|
||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||
recurringCount={recurringEventCount}
|
||||
schedulingType={eventType.schedulingType}
|
||||
users={[]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -239,15 +246,18 @@ function TimezoneDropdown({
|
||||
onChangeTimeFormat,
|
||||
onChangeTimeZone,
|
||||
timeZone,
|
||||
timeFormat,
|
||||
}: {
|
||||
onChangeTimeFormat: (newTimeFormat: string) => void;
|
||||
onChangeTimeZone: (newTimeZone: string) => void;
|
||||
timeZone?: string;
|
||||
timeFormat: string;
|
||||
}) {
|
||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
handleToggle24hClock(localStorage.getItem("timeOption.is24hClock") === "true");
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -264,18 +274,22 @@ function TimezoneDropdown({
|
||||
return (
|
||||
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||
<Collapsible.Trigger className="min-w-32 text-bookinglight mb-1 -ml-2 px-2 py-1 text-left dark:text-white">
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<GlobeIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
{timeZone}
|
||||
{isTimeOptionsOpen ? (
|
||||
<ChevronUpIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiChevronUp className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronDownIcon className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiChevronDown className="ml-1 -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</p>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} onToggle24hClock={handleToggle24hClock} />
|
||||
<TimeOptions
|
||||
onSelectTimeZone={handleSelectTimeZone}
|
||||
onToggle24hClock={handleToggle24hClock}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
@@ -289,10 +303,14 @@ const dateQuerySchema = z.object({
|
||||
|
||||
const useRouterQuery = <T extends string>(name: T) => {
|
||||
const router = useRouter();
|
||||
const query = z.object({ [name]: z.string().optional() }).parse(router.query);
|
||||
const existingQueryParams = router.asPath.split("?")[1];
|
||||
|
||||
const urlParams = new URLSearchParams(existingQueryParams);
|
||||
const query = Object.fromEntries(urlParams);
|
||||
|
||||
const setQuery = (newValue: string | number | null | undefined) => {
|
||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
return { [name]: query[name], setQuery } as {
|
||||
@@ -305,7 +323,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
const isEmbed = useIsEmbed();
|
||||
const query = dateQuerySchema.parse(router.query);
|
||||
const { rescheduleUid } = query;
|
||||
const { Theme } = useTheme(profile.theme);
|
||||
useTheme(profile.theme);
|
||||
const { t } = useLocale();
|
||||
const { contracts } = useContracts();
|
||||
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
|
||||
@@ -350,6 +368,8 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
}
|
||||
}, [telemetry]);
|
||||
|
||||
// get dynamic user list here
|
||||
const userList = eventType.users.map((user) => user.username).filter(notEmpty);
|
||||
// Recurring event sidebar requires more space
|
||||
const maxWidth = isAvailableTimesVisible
|
||||
? recurringEventCount
|
||||
@@ -361,22 +381,29 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
const timezoneDropdown = useMemo(
|
||||
() => (
|
||||
<TimezoneDropdown
|
||||
timeFormat={timeFormat}
|
||||
onChangeTimeFormat={setTimeFormat}
|
||||
timeZone={timeZone}
|
||||
onChangeTimeZone={setTimeZone}
|
||||
/>
|
||||
),
|
||||
[timeZone]
|
||||
[timeZone, timeFormat]
|
||||
);
|
||||
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 slug = rawSlug.join("/");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Theme />
|
||||
<HeadSeo
|
||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||
name={profile.name || undefined}
|
||||
username={profile.slug || undefined}
|
||||
username={slug || undefined}
|
||||
nextSeoProps={{
|
||||
nofollow: eventType.hidden,
|
||||
noindex: eventType.hidden,
|
||||
}}
|
||||
/>
|
||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||
<div>
|
||||
@@ -397,20 +424,10 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{/* mobile: details */}
|
||||
<div className="block px-4 pt-4 sm:p-8 md:hidden">
|
||||
<div>
|
||||
<AvatarGroup
|
||||
border="border-2 dark:border-gray-800 border-white"
|
||||
items={
|
||||
[
|
||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
||||
...eventType.users
|
||||
.filter((user) => user.name !== profile.name)
|
||||
.map((user) => ({
|
||||
title: user.name,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
alt: user.name || undefined,
|
||||
})),
|
||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||
}
|
||||
<UserAvatars
|
||||
profile={profile}
|
||||
users={eventType.users}
|
||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||
size={9}
|
||||
truncateAfter={5}
|
||||
/>
|
||||
@@ -420,16 +437,18 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
<h1 className="text-bookingdark mb-4 break-words text-xl font-semibold dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{eventType?.description && (
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
<div className="flex py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
<div>
|
||||
<Icon.FiInfo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
)}
|
||||
{eventType?.requiresConfirmation && (
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
{t("requires_confirmation")}
|
||||
</p>
|
||||
)}
|
||||
@@ -438,9 +457,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{Object.values(AppStoreLocationType).includes(
|
||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
||||
) ? (
|
||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiVideo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiMapPin className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
|
||||
{locationKeyToString(eventType.locations[0], t)}
|
||||
@@ -449,7 +468,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{eventType.locations.length > 1 && (
|
||||
<div className="flex-warp flex text-gray-600 dark:text-white">
|
||||
<div className="mr-[10px] ml-[2px] -mt-1 ">
|
||||
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiMapPin className="inline-block h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
<p>
|
||||
{eventType.locations.map((el, i, arr) => {
|
||||
@@ -466,12 +485,12 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-400" />
|
||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-500" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
@@ -482,9 +501,9 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<div className="ml-[27px]">
|
||||
<div className="flex items-center text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 shrink-0 text-gray-500" />
|
||||
<div>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
@@ -492,7 +511,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-sm text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
@@ -517,7 +536,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="text-gray-500 line-through dark:text-white">
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
{typeof booking.startTime === "string" &&
|
||||
parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
@@ -530,77 +549,67 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 sm:flex sm:py-5">
|
||||
<div className="overflow-hidden sm:flex">
|
||||
<div
|
||||
className={
|
||||
"hidden overflow-hidden pr-8 sm:border-r sm:dark:border-gray-700 md:flex md:flex-col " +
|
||||
"hidden overflow-hidden border-gray-200 p-5 sm:border-r sm:dark:border-gray-700 md:flex md:flex-col " +
|
||||
(isAvailableTimesVisible ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
|
||||
}>
|
||||
<AvatarGroup
|
||||
border="border-2 dark:border-gray-800 border-white"
|
||||
items={
|
||||
[
|
||||
{ image: profile.image, alt: profile.name, title: profile.name },
|
||||
...eventType.users
|
||||
.filter((user) => user.name !== profile.name)
|
||||
.map((user) => ({
|
||||
title: user.name,
|
||||
alt: user.name,
|
||||
image: `${CAL_URL}/${user.username}/avatar.png`,
|
||||
})),
|
||||
].filter((item) => !!item.image) as { image: string; alt?: string; title?: string }[]
|
||||
}
|
||||
<UserAvatars
|
||||
profile={profile}
|
||||
users={eventType.users}
|
||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||
size={10}
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-3 break-words font-medium text-gray-500 dark:text-gray-300">
|
||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-500 dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="font-cal mb-4 break-words text-xl font-semibold text-gray-900 dark:text-white">
|
||||
<h1 className="font-cal mb-6 break-words text-2xl text-gray-900 dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{eventType?.description && (
|
||||
<div className="flex text-gray-600 dark:text-white">
|
||||
<div className="flex py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
<div>
|
||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiInfo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
<p>{eventType.description}</p>
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
)}
|
||||
{eventType?.requiresConfirmation && (
|
||||
<div className="flex text-gray-600 dark:text-white">
|
||||
<div className="flex items-center text-sm font-medium text-gray-600 dark:text-white">
|
||||
<div>
|
||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
{t("requires_confirmation")}
|
||||
</div>
|
||||
)}
|
||||
{eventType.locations.length === 1 && (
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
{Object.values(AppStoreLocationType).includes(
|
||||
eventType.locations[0].type as unknown as AppStoreLocationType
|
||||
) ? (
|
||||
<VideoCameraIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiVideo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<LocationMarkerIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiMapPin className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
|
||||
{locationKeyToString(eventType.locations[0], t)}
|
||||
</p>
|
||||
)}
|
||||
{eventType.locations.length > 1 && (
|
||||
<div className="flex-warp flex text-gray-600 dark:text-white">
|
||||
<div className="flex-warp flex items-center font-medium text-gray-600 dark:text-white">
|
||||
<div className="mr-[10px] ml-[2px] -mt-1 ">
|
||||
<LocationMarkerIcon className="inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiMapPin className="inline-block h-4 w-4 text-gray-500" />
|
||||
</div>
|
||||
<p>
|
||||
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
{eventType.locations.map((el, i, arr) => {
|
||||
return (
|
||||
<span key={el.type}>
|
||||
{locationKeyToString(el, t)}{" "}
|
||||
{arr.length - 1 !== i && (
|
||||
<span className="font-light"> {t("or_lowercase")} </span>
|
||||
<span className="text-sm font-light"> {t("or_lowercase")} </span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
@@ -608,14 +617,14 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="py-1 text-sm font-medium text-gray-600 dark:text-white">
|
||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<div className="ml-[27px]">
|
||||
<div className="flex items-center text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-500" />
|
||||
<div>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
@@ -623,7 +632,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-sm text-gray-600 [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
@@ -639,7 +648,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
<p className="-ml-2 px-2 py-1 text-gray-600 dark:text-white">
|
||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
@@ -652,7 +661,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{timezoneDropdown}
|
||||
</div>
|
||||
|
||||
<GoBackToPreviousPage t={t} />
|
||||
{!isEmbed && <GoBackToPreviousPage t={t} />}
|
||||
|
||||
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
@@ -662,34 +671,27 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="text-gray-500 line-through dark:text-white">
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)*/}
|
||||
</div>
|
||||
{timeZone && (
|
||||
<SlotPicker
|
||||
weekStart={
|
||||
typeof profile.weekStart === "string"
|
||||
? ([
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
|
||||
: profile.weekStart /* Allows providing weekStart as number */
|
||||
}
|
||||
eventType={eventType}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
/>
|
||||
)}
|
||||
<SlotPicker
|
||||
weekStart={
|
||||
typeof profile.weekStart === "string"
|
||||
? (["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
profile.weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
|
||||
: profile.weekStart /* Allows providing weekStart as number */
|
||||
}
|
||||
eventType={eventType}
|
||||
timeFormat={timeFormat}
|
||||
timeZone={timeZone}
|
||||
users={userList}
|
||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||
recurringEventCount={recurringEventCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{(!eventType.users[0] || !isBrandingHidden(eventType.users[0])) && !isEmbed && <PoweredByCal />}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CreditCardIcon,
|
||||
ExclamationCircleIcon,
|
||||
ExclamationIcon,
|
||||
InformationCircleIcon,
|
||||
ClipboardCheckIcon,
|
||||
RefreshIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { EventTypeCustomInputType } from "@prisma/client";
|
||||
import { useContracts } from "contexts/contractsContext";
|
||||
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -24,35 +13,37 @@ import { useMutation } from "react-query";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { useContracts } from "@calcom/features/ee/web3/contexts/contractsContext";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { createPaymentLink } from "@calcom/stripe/client";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { LocationObject, LocationType } from "@lib/location";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||
import slugify from "@lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
|
||||
import CustomBranding from "@components/CustomBranding";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
||||
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||
|
||||
import { BookPageProps } from "../../../pages/[user]/book";
|
||||
import { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||
@@ -67,11 +58,6 @@ declare global {
|
||||
};
|
||||
}
|
||||
|
||||
/** These are like 40kb that not every user needs */
|
||||
const PhoneInput = dynamic(
|
||||
() => import("@components/ui/form/PhoneInput")
|
||||
) as unknown as typeof PhoneInputType;
|
||||
|
||||
type BookingPageProps = (BookPageProps | TeamBookingPageProps | HashLinkPageProps) & {
|
||||
locationLabels: Record<LocationType, string>;
|
||||
};
|
||||
@@ -88,6 +74,7 @@ type BookingFormValues = {
|
||||
[key: string]: string | boolean;
|
||||
};
|
||||
rescheduleReason?: string;
|
||||
smsReminderNumber?: string;
|
||||
};
|
||||
|
||||
const BookingPage = ({
|
||||
@@ -207,7 +194,7 @@ const BookingPage = ({
|
||||
});
|
||||
|
||||
const rescheduleUid = router.query.rescheduleUid as string;
|
||||
const { isReady, Theme } = useTheme(profile.theme);
|
||||
useTheme(profile.theme);
|
||||
const date = asStringOrNull(router.query.date);
|
||||
|
||||
const [guestToggle, setGuestToggle] = useState(booking && booking.attendees.length > 1);
|
||||
@@ -232,11 +219,25 @@ const BookingPage = ({
|
||||
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
||||
: [];
|
||||
|
||||
// There should only exists one default userData variable for primaryAttendee.
|
||||
const defaultUserValues = {
|
||||
email: booking?.attendees[0].email
|
||||
? booking.attendees[0].email
|
||||
: router.query.email
|
||||
? (router.query.email as string)
|
||||
: "",
|
||||
name: booking?.attendees[0].name
|
||||
? booking.attendees[0].name
|
||||
: router.query.name
|
||||
? (router.query.name as string)
|
||||
: "",
|
||||
};
|
||||
|
||||
const defaultValues = () => {
|
||||
if (!rescheduleUid) {
|
||||
return {
|
||||
name: (router.query.name as string) || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: (router.query.email as string) || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
notes: (router.query.notes as string) || "",
|
||||
guests: ensureArray(router.query.guest) as string[],
|
||||
customInputs: eventType.customInputs.reduce(
|
||||
@@ -258,8 +259,8 @@ const BookingPage = ({
|
||||
|
||||
const customInputType = booking.customInputs;
|
||||
return {
|
||||
name: primaryAttendee.name || "",
|
||||
email: primaryAttendee.email || "",
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email || "",
|
||||
guests: guestListEmails,
|
||||
notes: booking.description || "",
|
||||
rescheduleReason: "",
|
||||
@@ -283,6 +284,10 @@ const BookingPage = ({
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
smsReminderNumber: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -321,6 +326,18 @@ const BookingPage = ({
|
||||
case LocationType.UserPhone: {
|
||||
return locationInfo(locationType)?.hostPhoneNumber || "";
|
||||
}
|
||||
case LocationType.Around: {
|
||||
return locationInfo(locationType)?.link || "";
|
||||
}
|
||||
case LocationType.Riverside: {
|
||||
return locationInfo(locationType)?.link || "";
|
||||
}
|
||||
case LocationType.Whereby: {
|
||||
return locationInfo(locationType)?.link || "";
|
||||
}
|
||||
case LocationType.Ping: {
|
||||
return locationInfo(locationType)?.link || "";
|
||||
}
|
||||
// Catches all other location types, such as Google Meet, Zoom etc.
|
||||
default:
|
||||
return selectedLocation || "";
|
||||
@@ -396,6 +413,8 @@ const BookingPage = ({
|
||||
})),
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
||||
}));
|
||||
recurringMutation.mutate(recurringBookings);
|
||||
} else {
|
||||
@@ -421,18 +440,35 @@ const BookingPage = ({
|
||||
})),
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
selectedLocation === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const disableInput = !!rescheduleUid;
|
||||
// Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields.
|
||||
const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name;
|
||||
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
|
||||
const inputClassName =
|
||||
"focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 sm:text-sm";
|
||||
"focus:border-brand block w-full rounded-sm border-gray-300 focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 text-sm";
|
||||
|
||||
let isSmsReminderNumberNeeded = false;
|
||||
|
||||
if (eventType.workflows.length > 0) {
|
||||
eventType.workflows.forEach((workflowReference) => {
|
||||
if (workflowReference.workflow.steps.length > 0) {
|
||||
workflowReference.workflow.steps.forEach((step) => {
|
||||
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
||||
isSmsReminderNumberNeeded = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Theme />
|
||||
<Head>
|
||||
<title>
|
||||
{rescheduleUid
|
||||
@@ -455,402 +491,416 @@ const BookingPage = ({
|
||||
isEmbed ? "" : "sm:my-24",
|
||||
"my-0 max-w-3xl "
|
||||
)}>
|
||||
{isReady && (
|
||||
<div
|
||||
className={classNames(
|
||||
"main overflow-hidden",
|
||||
isEmbed ? "" : "border border-gray-200",
|
||||
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
|
||||
"rounded-md dark:border-gray-600 sm:border"
|
||||
)}>
|
||||
<div className="px-4 py-5 sm:flex sm:p-4">
|
||||
<div className="sm:w-1/2 sm:border-r sm:dark:border-gray-700">
|
||||
<AvatarGroup
|
||||
border="border-2 border-white dark:border-gray-800"
|
||||
size={14}
|
||||
items={[{ image: profile.image || "", alt: profile.name || "" }].concat(
|
||||
eventType.users
|
||||
.filter((user) => user.name !== profile.name)
|
||||
.map((user) => ({
|
||||
image: user.avatar || "",
|
||||
alt: user.name || "",
|
||||
}))
|
||||
)}
|
||||
/>
|
||||
<h2 className="font-cal text-bookinglight mt-2 font-medium dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} mb-2`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
{eventType?.description && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
)}
|
||||
{eventType?.requiresConfirmation && (
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<ClipboardCheckIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{t("requires_confirmation")}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-bookinglight mb-2 dark:text-white">
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
<div
|
||||
className={classNames(
|
||||
"main overflow-hidden",
|
||||
isEmbed ? "" : "border border-gray-200",
|
||||
isBackgroundTransparent ? "" : "dark:border-1 bg-white dark:bg-gray-800",
|
||||
"rounded-md dark:border-gray-600 sm:border"
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
<div className="px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6 sm:dark:border-gray-700">
|
||||
<UserAvatars
|
||||
profile={profile}
|
||||
users={eventType.users}
|
||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||
size={14}
|
||||
/>
|
||||
<h2 className="font-cal text-bookinglight mt-2 font-medium dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="text-bookingdark mb-4 text-xl font-semibold dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} mb-2`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1 dark:text-white">
|
||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
style="currency"
|
||||
currency={eventType.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)}
|
||||
{eventType?.description && (
|
||||
<p className="text-bookinglight mb-2 text-sm dark:text-white">
|
||||
<Icon.FiInfo className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
)}
|
||||
{eventType?.requiresConfirmation && (
|
||||
<p className="text-bookinglight mb-2 text-sm dark:text-white">
|
||||
<Icon.FiClipboard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{t("requires_confirmation")}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-bookinglight mb-2 text-sm dark:text-white">
|
||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1 text-sm dark:text-white">
|
||||
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
style="currency"
|
||||
currency={eventType.currency.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="mb-3 text-sm text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCw className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="mb-3 text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-bookinghighlight mb-4 flex items-center text-sm">
|
||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||
<div>
|
||||
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
parseDate(dayjs(date).tz(timeZone()), i18n)}
|
||||
{!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.slice(5).map((aDate, key) => (
|
||||
<p key={key}>{aDate}</p>
|
||||
))}>
|
||||
<p className="text-sm text-gray-600 dark:text-white">
|
||||
{t("plus_more", { count: recurringStrings.length - 5 })}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
|
||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1">
|
||||
{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}
|
||||
</p>
|
||||
)}
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p className="mt-8 mb-2 text-sm text-gray-600 dark:text-white" data-testid="former_time_p">
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="text-gray-500 line-through dark:text-white">
|
||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-6 sm:w-1/2">
|
||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("your_name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
{...bookingForm.register("name", { required: true })}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
required
|
||||
className={inputClassName}
|
||||
placeholder={t("example_name")}
|
||||
disabled={disableInput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-bookinghighlight mb-4 flex">
|
||||
<CalendarIcon className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
|
||||
<div className="-mt-1">
|
||||
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
parseDate(dayjs(date).tz(timeZone()), i18n)}
|
||||
{!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.slice(5).map((aDate, key) => (
|
||||
<p key={key}>{aDate}</p>
|
||||
))}>
|
||||
<p className="text-gray-600 dark:text-white">
|
||||
{t("plus_more", { count: recurringStrings.length - 5 })}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("email_address")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<EmailInput
|
||||
{...bookingForm.register("email")}
|
||||
required
|
||||
className={classNames(
|
||||
inputClassName,
|
||||
bookingForm.formState.errors.email
|
||||
? "border-red-700 focus:ring-red-700"
|
||||
: " border-gray-300 dark:border-gray-900"
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
||||
disabled={disableInput}
|
||||
/>
|
||||
{bookingForm.formState.errors.email && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<p>{t("email_validation_error")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (
|
||||
<p className="text-bookinglight mb-1 -ml-2 px-2 py-1">
|
||||
{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}
|
||||
</p>
|
||||
)}
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
<p className="mt-8 mb-2 text-gray-600 dark:text-white" data-testid="former_time_p">
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="text-gray-500 line-through dark:text-white">
|
||||
<CalendarIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||
{locations.length > 1 && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("your_name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
{...bookingForm.register("name", { required: true })}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
required
|
||||
className={inputClassName}
|
||||
placeholder={t("example_name")}
|
||||
disabled={disableInput}
|
||||
/>
|
||||
</div>
|
||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("location")}
|
||||
</span>
|
||||
{locations.map((location, i) => (
|
||||
<label key={i} className="block">
|
||||
<input
|
||||
type="radio"
|
||||
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
||||
{...bookingForm.register("locationType", { required: true })}
|
||||
value={location.type}
|
||||
defaultChecked={selectedLocation === location.type}
|
||||
/>
|
||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
|
||||
{locationLabels[location.type]}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedLocation === LocationType.Phone && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="email"
|
||||
htmlFor="phone"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("email_address")}
|
||||
{t("phone_number")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<EmailInput
|
||||
{...bookingForm.register("email")}
|
||||
<PhoneInput<BookingFormValues>
|
||||
control={bookingForm.control}
|
||||
name="phone"
|
||||
placeholder={t("enter_phone_number")}
|
||||
id="phone"
|
||||
required
|
||||
className={classNames(
|
||||
inputClassName,
|
||||
bookingForm.formState.errors.email
|
||||
? "border-red-700 focus:ring-red-700"
|
||||
: " border-gray-300 dark:border-gray-900"
|
||||
)}
|
||||
placeholder="you@example.com"
|
||||
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
||||
disabled={disableInput}
|
||||
/>
|
||||
{bookingForm.formState.errors.email && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
|
||||
<p>{t("email_validation_error")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{locations.length > 1 && (
|
||||
<div className="mb-4">
|
||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("location")}
|
||||
</span>
|
||||
{locations.map((location, i) => (
|
||||
<label key={i} className="block">
|
||||
<input
|
||||
type="radio"
|
||||
className="location h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
||||
{...bookingForm.register("locationType", { required: true })}
|
||||
value={location.type}
|
||||
defaultChecked={selectedLocation === location.type}
|
||||
/>
|
||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
|
||||
{locationLabels[location.type]}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{selectedLocation === LocationType.Phone && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("phone_number")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<PhoneInput<BookingFormValues>
|
||||
control={bookingForm.control}
|
||||
name="phone"
|
||||
placeholder={t("enter_phone_number")}
|
||||
id="phone"
|
||||
required
|
||||
disabled={disableInput}
|
||||
/>
|
||||
{bookingForm.formState.errors.phone && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
{bookingForm.formState.errors.phone && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<ExclamationCircleIcon className="mr-2 h-3 w-3" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{eventType.customInputs
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((input) => (
|
||||
<div className="mb-4" key={input.id}>
|
||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{eventType.customInputs
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map((input) => (
|
||||
<div className="mb-4" key={input.id}>
|
||||
{input.type !== EventTypeCustomInputType.BOOL && (
|
||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||
<textarea
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={input.placeholder}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXT && (
|
||||
<input
|
||||
type="text"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className={inputClassName}
|
||||
placeholder={input.placeholder}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.NUMBER && (
|
||||
<input
|
||||
type="number"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className={inputClassName}
|
||||
placeholder=""
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.BOOL && (
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
||||
placeholder=""
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXTLONG && (
|
||||
<textarea
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={input.placeholder}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.TEXT && (
|
||||
<input
|
||||
type="text"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className={inputClassName}
|
||||
placeholder={input.placeholder}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.NUMBER && (
|
||||
<input
|
||||
type="number"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className={inputClassName}
|
||||
placeholder=""
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.BOOL && (
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
{...bookingForm.register(`customInputs.${input.id}`, {
|
||||
required: input.required,
|
||||
})}
|
||||
id={"custom_" + input.id}
|
||||
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black disabled:bg-gray-200 ltr:mr-2 rtl:ml-2 disabled:dark:text-gray-500"
|
||||
placeholder=""
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
<label
|
||||
htmlFor={"custom_" + input.id}
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{input.label}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!eventType.disableGuests && (
|
||||
<div className="mb-4">
|
||||
{!guestToggle && (
|
||||
<label
|
||||
onClick={() => setGuestToggle(!guestToggle)}
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium hover:cursor-pointer dark:text-white">
|
||||
{/*<UserAddIcon className="inline-block w-5 h-5 mr-1 -mt-1" />*/}
|
||||
{t("additional_guests")}
|
||||
</label>
|
||||
)}
|
||||
{guestToggle && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
{!disableInput && (
|
||||
<Controller
|
||||
control={bookingForm.control}
|
||||
name="guests"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder="guest@example.com"
|
||||
emails={value}
|
||||
onChange={onChange}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index} className="cursor-pointer">
|
||||
{email}
|
||||
{!disableInput && (
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Custom code when guest emails should not be editable */}
|
||||
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
||||
<div data-tag className="react-multi-email">
|
||||
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
||||
{guestListEmails.map((email, index) => {
|
||||
return (
|
||||
<div key={index} className="cursor-pointer">
|
||||
<span data-tag>{email}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
{!eventType.disableGuests && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="notes"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
|
||||
</label>
|
||||
{rescheduleUid ? (
|
||||
<textarea
|
||||
{...bookingForm.register("rescheduleReason")}
|
||||
id="rescheduleReason"
|
||||
name="rescheduleReason"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("reschedule_placeholder")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
{...bookingForm.register("notes")}
|
||||
id="notes"
|
||||
name="notes"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("share_additional_notes")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
{!guestToggle && (
|
||||
<label
|
||||
onClick={() => setGuestToggle(!guestToggle)}
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium hover:cursor-pointer dark:text-white">
|
||||
{/*<UserAddIcon className="inline-block w-5 h-5 mr-1 -mt-1" />*/}
|
||||
{t("additional_guests")}
|
||||
</label>
|
||||
)}
|
||||
{guestToggle && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
{!disableInput && (
|
||||
<Controller
|
||||
control={bookingForm.control}
|
||||
name="guests"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder="guest@example.com"
|
||||
emails={value}
|
||||
onChange={onChange}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index} className="cursor-pointer">
|
||||
{email}
|
||||
{!disableInput && (
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Custom code when guest emails should not be editable */}
|
||||
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
||||
<div data-tag className="react-multi-email">
|
||||
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
||||
{guestListEmails.map((email, index) => {
|
||||
return (
|
||||
<div key={index} className="cursor-pointer">
|
||||
<span data-tag>{email}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
</Button>
|
||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{(mutation.isError || recurringMutation.isError) && (
|
||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||
)}
|
||||
</div>
|
||||
{isSmsReminderNumberNeeded && selectedLocation !== LocationType.Phone && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="smsReminderNumber"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("number_for_sms_reminders")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<PhoneInput<BookingFormValues>
|
||||
control={bookingForm.control}
|
||||
name="smsReminderNumber"
|
||||
placeholder={t("enter_phone_number")}
|
||||
id="smsReminderNumber"
|
||||
required
|
||||
disabled={disableInput}
|
||||
/>
|
||||
</div>
|
||||
{bookingForm.formState.errors.smsReminderNumber && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="notes"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{rescheduleUid ? t("reschedule_optional") : t("additional_notes")}
|
||||
</label>
|
||||
{rescheduleUid ? (
|
||||
<textarea
|
||||
{...bookingForm.register("rescheduleReason")}
|
||||
id="rescheduleReason"
|
||||
name="rescheduleReason"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("reschedule_placeholder")}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
{...bookingForm.register("notes")}
|
||||
id="notes"
|
||||
name="notes"
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("share_additional_notes")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
</Button>
|
||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{(mutation.isError || recurringMutation.isError) && (
|
||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
@@ -866,7 +916,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
<Icon.FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ExclamationIcon } from "@heroicons/react/outline";
|
||||
import { CheckIcon } from "@heroicons/react/solid";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
export type DeleteStripeDialogContentProps = {
|
||||
cancelAllBookingsBtnText?: string;
|
||||
@@ -37,17 +36,17 @@ export default function DeleteStripeDialogContent(props: PropsWithChildren<Delet
|
||||
<div className="mt-0.5 ltr:mr-3">
|
||||
{variety === "danger" && (
|
||||
<div className="mx-auto rounded-full bg-red-100 p-2 text-center">
|
||||
<ExclamationIcon className="h-5 w-5 text-red-600" />
|
||||
<Icon.FiAlertTriangle className="h-5 w-5 text-red-600" />
|
||||
</div>
|
||||
)}
|
||||
{variety === "warning" && (
|
||||
<div className="mx-auto rounded-full bg-orange-100 p-2 text-center">
|
||||
<ExclamationIcon className="h-5 w-5 text-orange-600" />
|
||||
<Icon.FiAlertTriangle className="h-5 w-5 text-orange-600" />
|
||||
</div>
|
||||
)}
|
||||
{variety === "success" && (
|
||||
<div className="mx-auto rounded-full bg-green-100 p-2 text-center">
|
||||
<CheckIcon className="h-5 w-5 text-green-600" />
|
||||
<Icon.FiCheck className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
import { LocationMarkerIcon } from "@heroicons/react/solid";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { LocationOptionsToString } from "@calcom/app-store/locations";
|
||||
import { LocationType } from "@calcom/core/location";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||
import { Form } from "@calcom/ui/form/fields";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { linkValueToString } from "@lib/linkValueToString";
|
||||
import { LocationType } from "@lib/location";
|
||||
import { LocationOptionsToString } from "@lib/locationOptions";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import type PhoneInputType from "@components/ui/form/PhoneInput";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
const PhoneInput = dynamic(
|
||||
() => import("@components/ui/form/PhoneInput")
|
||||
) as unknown as typeof PhoneInputType;
|
||||
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
|
||||
type OptionTypeBase = {
|
||||
@@ -91,6 +85,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
.string()
|
||||
.regex(/^http(s)?:\/\/(www\.)?around.co\/[a-zA-Z0-9]*/)
|
||||
.optional()
|
||||
: selection?.value === LocationType.Ping
|
||||
? z
|
||||
.string()
|
||||
.regex(/^http(s)?:\/\/(www\.)?ping.gg\/call\/[a-zA-Z0-9]*/)
|
||||
.optional()
|
||||
: selection?.value === LocationType.Riverside
|
||||
? z
|
||||
.string()
|
||||
@@ -154,7 +153,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
{...locationFormMethods.register("locationLink")}
|
||||
required
|
||||
id="link"
|
||||
className="block w-full rounded-sm border-gray-300 sm:text-sm"
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
@@ -200,7 +200,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) => locationFormMethods.setValue("displayLocationPublicly", e.target.checked)}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -245,7 +246,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
id="wherebylink"
|
||||
placeholder="https://www.whereby.com/cal"
|
||||
required
|
||||
className={"block w-full rounded-sm border-gray-300 text-sm"}
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
@@ -254,6 +255,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationLink && (
|
||||
<p className="mt-1 text-sm text-red-500">{t("invalid_whereby_link")}</p>
|
||||
)}
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
@@ -272,7 +276,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -292,7 +297,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
id="aroundlink"
|
||||
placeholder="https://www.around.co/rick"
|
||||
required
|
||||
className={"block w-full rounded-sm border-gray-300 text-sm"}
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
@@ -301,6 +306,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationLink && (
|
||||
<p className="mt-1 text-sm text-red-500">{t("invalid_around_link")}</p>
|
||||
)}
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
@@ -319,7 +327,59 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : selectedLocation === LocationType.Ping ? (
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||
{t("set_ping_link")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
{...locationFormMethods.register("locationLink")}
|
||||
id="pinglink"
|
||||
placeholder="https://www.ping.gg/call/theo"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
(location: { type: LocationType }) => location.type === LocationType.Ping
|
||||
)?.address
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationLink && (
|
||||
<p className="mt-1 text-sm text-red-500">{t("invalid_ping_link")}</p>
|
||||
)}
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
<Controller
|
||||
name="displayLocationPublicly"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<CheckboxField
|
||||
defaultChecked={
|
||||
defaultValues
|
||||
? defaultValues.find((location) => location.type === LocationType.Ping)
|
||||
?.displayLocationPublicly
|
||||
: undefined
|
||||
}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -330,7 +390,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="address" className="block text-sm font-medium text-gray-700">
|
||||
{t("set_around_link")}
|
||||
{t("set_riverside_link")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
@@ -339,7 +399,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
id="aroundlink"
|
||||
placeholder="https://www.riverside.fm/studio/rick"
|
||||
required
|
||||
className={"block w-full rounded-sm border-gray-300 text-sm"}
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
defaultValue={
|
||||
defaultValues
|
||||
? defaultValues.find(
|
||||
@@ -348,6 +408,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{locationFormMethods.formState.errors.locationLink && (
|
||||
<p className="mt-1 text-sm text-red-500">{t("invalid_riverside_link")}</p>
|
||||
)}
|
||||
</div>
|
||||
{!booking && (
|
||||
<div className="mt-3">
|
||||
@@ -366,7 +429,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
onChange={(e) =>
|
||||
locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
|
||||
}
|
||||
informationIconText={t("display_location_info_badge")}></CheckboxField>
|
||||
informationIconText={t("display_location_info_badge")}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -383,7 +447,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<div className="inline-block transform rounded-sm bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<LocationMarkerIcon className="text-primary-600 h-6 w-6" />
|
||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
@@ -393,7 +457,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"></div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" />
|
||||
</div>
|
||||
{booking && (
|
||||
<>
|
||||
@@ -417,7 +481,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
newLocation === LocationType.Link ||
|
||||
newLocation === LocationType.Whereby ||
|
||||
newLocation === LocationType.Around ||
|
||||
newLocation === LocationType.Riverside
|
||||
newLocation === LocationType.Riverside ||
|
||||
newLocation === LocationType.Ping
|
||||
) {
|
||||
details = { link: values.locationLink, displayLocationPublicly };
|
||||
}
|
||||
@@ -454,8 +519,8 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
? locationOptions.filter((location) => location.value !== "phone")
|
||||
: locationOptions
|
||||
}
|
||||
isSearchable={false}
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 sm:text-sm"
|
||||
isSearchable
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ClockIcon } from "@heroicons/react/outline";
|
||||
import { RescheduleResponse } from "pages/api/book/request-reschedule";
|
||||
import React, { useState, Dispatch, SetStateAction } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { TextArea } from "@calcom/ui/form/fields";
|
||||
|
||||
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -58,7 +58,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<ClockIcon className="m-auto h-6 w-6"></ClockIcon>
|
||||
<Icon.FiClock className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
|
||||
@@ -19,11 +22,11 @@ import Dropdown, {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { slugify } from "@lib/slugify";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import * as RadioArea from "@components/ui/form/radio-area";
|
||||
@@ -36,7 +39,7 @@ export interface EventTypeParent {
|
||||
image?: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
interface CreateEventTypeBtnProps {
|
||||
// set true for use on the team settings page
|
||||
canAddEvents: boolean;
|
||||
// set true when in use on the team settings page
|
||||
@@ -45,7 +48,7 @@ interface Props {
|
||||
options: EventTypeParent[];
|
||||
}
|
||||
|
||||
export default function CreateEventTypeButton(props: Props) {
|
||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -141,41 +144,15 @@ export default function CreateEventTypeButton(props: Props) {
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={["eventPage", "teamId", "type", "description", "title", "length", "slug"]}>
|
||||
{!hasTeams || props.isIndividualTeam ? (
|
||||
<Button
|
||||
onClick={() => openModal(props.options[0])}
|
||||
data-testid="new-event-type"
|
||||
StartIcon={PlusIcon}
|
||||
disabled={!props.canAddEvents}>
|
||||
{t("new_event_type_btn")}
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{props.options.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.slug}
|
||||
className="cursor-pointer px-3 py-2 hover:bg-neutral-100 focus:outline-none"
|
||||
onSelect={() => openModal(option)}>
|
||||
<Avatar
|
||||
alt={option.name || ""}
|
||||
imageSrc={option.image}
|
||||
size={6}
|
||||
className="inline ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
{option.name ? option.name : option.slug}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
)}
|
||||
<CreateEventTypeTrigger
|
||||
hasTeams={hasTeams}
|
||||
canAddEvents={props.canAddEvents}
|
||||
isIndividualTeam={props.isIndividualTeam}
|
||||
openModal={openModal}
|
||||
options={props.options}
|
||||
/>
|
||||
|
||||
<DialogContent className="overflow-y-auto">
|
||||
<DialogContent>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
@@ -257,7 +234,7 @@ export default function CreateEventTypeButton(props: Props) {
|
||||
<RadioArea.Group
|
||||
{...register("schedulingType")}
|
||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
||||
className="relative mt-1 flex space-x-6 rounded-sm shadow-sm rtl:space-x-reverse">
|
||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
||||
@@ -289,3 +266,83 @@ export default function CreateEventTypeButton(props: Props) {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
type CreateEventTypeTrigger = {
|
||||
isIndividualTeam?: boolean;
|
||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
||||
options: EventTypeParent[];
|
||||
hasTeams: boolean;
|
||||
// set true for use on the team settings page
|
||||
canAddEvents: boolean;
|
||||
openModal: (option: EventTypeParent) => void;
|
||||
};
|
||||
|
||||
export function CreateEventTypeTrigger(props: CreateEventTypeTrigger) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!props.hasTeams || props.isIndividualTeam ? (
|
||||
<Button
|
||||
onClick={() => props.openModal(props.options[0])}
|
||||
data-testid="new-event-type"
|
||||
StartIcon={Icon.FiPlus}
|
||||
disabled={!props.canAddEvents}>
|
||||
{t("new_event_type_btn")}
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button EndIcon={Icon.FiChevronDown}>{t("new_event_type_btn")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{props.options.map((option) => (
|
||||
<CreateEventTeamsItem
|
||||
key={option.slug}
|
||||
option={option}
|
||||
openModal={() => props.openModal(option)}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateEventTeamsItem(props: {
|
||||
openModal: (option: EventTypeParent) => void;
|
||||
option: EventTypeParent;
|
||||
}) {
|
||||
const session = useSession();
|
||||
const membershipQuery = trpc.useQuery([
|
||||
"viewer.teams.getMembershipbyUser",
|
||||
{
|
||||
memberId: session.data?.user.id as number,
|
||||
teamId: props.option.teamId as number,
|
||||
},
|
||||
]);
|
||||
|
||||
const isDisabled = membershipQuery.data?.role === "MEMBER";
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={props.option.slug}
|
||||
className={classNames(
|
||||
"cursor-pointer px-3 py-2 focus:outline-none",
|
||||
isDisabled ? "cursor-default !text-gray-300" : "hover:bg-neutral-100"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
onSelect={() => props.openModal(props.option)}>
|
||||
<Avatar
|
||||
alt={props.option.name || ""}
|
||||
imageSrc={props.option.image}
|
||||
size={6}
|
||||
className="inline ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
{props.option.name ? props.option.name : props.option.slug}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
ClipboardCheckIcon,
|
||||
ClockIcon,
|
||||
CreditCardIcon,
|
||||
RefreshIcon,
|
||||
UserIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { Prisma, SchedulingType } from "@prisma/client";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
@@ -13,6 +5,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -44,34 +37,34 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
||||
{eventType.description.length > 100 && "..."}
|
||||
</h2>
|
||||
)}
|
||||
<ul className="mt-2 flex flex-wrap sm:flex-nowrap">
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<ClockIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
{eventType.length}m
|
||||
<ul className="mt-2 flex flex-wrap space-x-1 sm:flex-nowrap ">
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiClock className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</li>
|
||||
{eventType.schedulingType ? (
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<UsersIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiUsers className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
||||
</li>
|
||||
) : (
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<UserIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiUser className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("1_on_1")}
|
||||
</li>
|
||||
)}
|
||||
{recurringEvent?.count && recurringEvent.count > 0 && (
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<RefreshIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiRefreshCw className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("repeats_up_to", {
|
||||
count: recurringEvent.count,
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<CreditCardIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiCreditCard className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
@@ -82,8 +75,8 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
||||
</li>
|
||||
)}
|
||||
{eventType.requiresConfirmation && (
|
||||
<li className="mr-4 mb-1 flex items-center whitespace-nowrap">
|
||||
<ClipboardCheckIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
<li className="mb-1 flex items-center whitespace-nowrap rounded-sm bg-gray-100 px-1 py-px text-xs text-gray-800 dark:bg-gray-900 dark:text-white">
|
||||
<Icon.FiCheckSquare className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
|
||||
{t("requires_confirmation")}
|
||||
</li>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export type EventTypeDescriptionSafeProps = {
|
||||
eventType: { description: string | null };
|
||||
};
|
||||
|
||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
||||
return <div {...props} />;
|
||||
};
|
||||
|
||||
export default EventTypeDescriptionSafeHTML;
|
||||
@@ -4,7 +4,7 @@ import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
@@ -82,7 +82,7 @@ export default function RecurringEventController({
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
||||
className="block w-16 rounded-sm border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.interval}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
@@ -97,7 +97,7 @@ export default function RecurringEventController({
|
||||
options={recurringEventFreqOptions}
|
||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||
isSearchable={false}
|
||||
className="w-18 block min-w-0 rounded-sm sm:text-sm"
|
||||
className="w-18 block min-w-0 rounded-sm text-sm"
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
@@ -114,7 +114,7 @@ export default function RecurringEventController({
|
||||
type="number"
|
||||
min="1"
|
||||
max="20"
|
||||
className="block w-16 rounded-sm border-gray-300 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 sm:text-sm"
|
||||
className="block w-16 rounded-sm border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
defaultValue={recurringEventState.count}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user