Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6415d42a1d | ||
|
|
ee620c7fff | ||
|
|
112efbbda2 | ||
|
|
fac0b0fa32 | ||
|
|
12d47ab949 | ||
|
|
4fef7ba5e2 | ||
|
|
6af0428a18 | ||
|
|
d66f3d1dc9 | ||
|
|
a5249b6cb5 | ||
|
|
6771c11360 | ||
|
|
002375ac72 | ||
|
|
0402e46328 | ||
|
|
02e3738a17 | ||
|
|
f822177788 | ||
|
|
1edc9c50d5 | ||
|
|
7beb17c007 | ||
|
|
ad7af451ff | ||
|
|
f513ce8687 | ||
|
|
d83883e7e4 | ||
|
|
9e001c53e8 | ||
|
|
3937b0c4c6 | ||
|
|
cbe358bb6b | ||
|
|
2b91102f3f | ||
|
|
4e7804c46d | ||
|
|
d7226fc3d4 | ||
|
|
0cb278d109 | ||
|
|
a1430101d1 | ||
|
|
9092addd9c | ||
|
|
7f14e00119 | ||
|
|
abc5ff2d97 | ||
|
|
fcde1f7a9d | ||
|
|
30eea089ba | ||
|
|
3c8cb3a9ea | ||
|
|
54f4e665a3 | ||
|
|
ef3e7fae20 | ||
|
|
bb7815464c | ||
|
|
acb6f292ec | ||
|
|
0b2250caf9 | ||
|
|
db7c3fb52f | ||
|
|
6d67808627 | ||
|
|
557b57a261 | ||
|
|
31875f7535 | ||
|
|
29c4efe4a8 | ||
|
|
7bda15aaa3 | ||
|
|
4176086ce0 | ||
|
|
04e62a10e6 | ||
|
|
0ff38dece2 | ||
|
|
8416b76bf6 | ||
|
|
c3a427346d | ||
|
|
a73b3fcaed | ||
|
|
25d684ed3e | ||
|
|
195cb84a8b | ||
|
|
4466c4ad4b | ||
|
|
4b532646cb | ||
|
|
6d42fdb71c | ||
|
|
1b23671812 | ||
|
|
7d1fb7c659 | ||
|
|
97b99deb5d | ||
|
|
4803c956dc | ||
|
|
8ff5d476d9 | ||
|
|
4421981da7 | ||
|
|
87ffedbebd | ||
|
|
2ba5e6cdd4 | ||
|
|
69100a0fe8 | ||
|
|
cde02e1a47 | ||
|
|
deeb8f38e5 | ||
|
|
85d7341668 | ||
|
+1 |
8593da4e25 | ||
|
|
8b5d57c573 | ||
|
|
7e19e0d9e2 | ||
|
|
47a663f5a9 | ||
|
|
e5ba861621 | ||
|
|
82a6ab7957 | ||
|
|
22dad1ae7c | ||
|
|
f1208eadc0 | ||
|
|
d9a867bbde | ||
|
|
7625950950 | ||
|
|
bf68ec1ad5 | ||
|
|
0fa7982084 | ||
|
|
a419613d29 | ||
|
|
dff08dc5b9 | ||
|
|
7517feb62a | ||
|
|
5954144b98 | ||
|
|
a8c4a9c4c2 | ||
|
|
b1bef50ba8 | ||
|
|
5030245ad5 | ||
|
|
2aa84cd5ce | ||
|
|
324794e36c | ||
|
|
25e3e31045 | ||
|
+6 |
9d1a0637f4 | ||
|
|
b5b41da183 | ||
|
|
acad0619f5 | ||
|
|
db9911a264 | ||
|
|
328a354f4d | ||
|
|
eb14c1b796 | ||
|
|
179a9b7139 | ||
|
|
3f486a9b99 | ||
|
|
b0f2ce4818 | ||
|
|
d7e9bb10b8 | ||
|
|
4c24614e06 | ||
|
|
6a002b900f | ||
|
|
d751cca0f4 | ||
|
|
328f00a9b5 | ||
|
|
e6a5fe179b | ||
|
|
b629333eb8 | ||
|
|
06eca4b28a | ||
|
|
f0ca797902 | ||
|
+6 |
3efd062dbf | ||
|
|
0959cf7cd5 | ||
|
|
f34d6e3c17 | ||
|
|
edabf27929 | ||
|
|
98f6fd836e | ||
|
|
a8710ce625 | ||
|
|
3043294a08 | ||
|
|
66c2537f9f | ||
|
|
b3d74e575a | ||
|
|
190d0435e0 | ||
|
|
d0338dc71b | ||
|
|
2df8f4f145 | ||
|
|
8ecfe89a6f | ||
|
|
b1ced10626 | ||
|
|
b3ba89c074 | ||
|
|
77d339ae8f | ||
|
|
ac9b2d0577 | ||
|
|
03fd546e99 | ||
|
|
6484f1639c | ||
|
|
f934dc6f4f | ||
|
|
dd9d32815a | ||
|
|
5c2ad7827e | ||
|
|
5dc5eadf28 | ||
|
|
d6fc551950 | ||
|
|
5bb868d2c3 | ||
|
|
ec9b3a4261 | ||
|
|
ce04f78a56 | ||
|
|
9f362db60c | ||
|
|
7a93bec735 | ||
|
|
7b1e824a36 | ||
|
|
0746b1e372 | ||
|
|
39747a151c | ||
|
|
b6ae4529e9 | ||
|
|
8c76da8d79 | ||
|
|
4210f35e9c | ||
|
+9 |
eae60043bb | ||
|
|
bf34296864 | ||
|
|
d0a24f7c3c | ||
|
|
f009321deb | ||
|
|
4d04a4d9a6 | ||
|
|
888f60a94c | ||
|
|
d6c4970bf4 | ||
|
|
63ce4a68ac | ||
|
|
cddcd742f6 | ||
|
|
a7a82a6b51 | ||
|
|
1a104f172d | ||
|
|
142be70b89 | ||
|
|
5a0e4c8726 | ||
|
|
f2cdb75f91 | ||
|
|
352ce35b40 | ||
|
|
3fa52b0600 | ||
|
|
a4ff5d0cee | ||
|
|
5f1d20319a | ||
|
|
d597aa91e5 | ||
|
|
803ab6c2e9 | ||
|
|
0a23be4579 | ||
|
|
00c3e9680c | ||
|
|
68ae3c48a0 | ||
|
|
1c59c0dd06 | ||
|
|
2d5bf1ffc1 | ||
|
|
317909d20a | ||
|
|
16eb516a9b | ||
|
|
a0e93e1eeb | ||
|
|
b18eabc6e8 | ||
|
|
5305f31266 | ||
|
|
6232a111ef | ||
|
|
0ea58103c8 | ||
|
|
3d595e9299 | ||
|
|
f135f7baad | ||
|
|
9f1341e94e | ||
|
|
79df03de49 | ||
|
+9 |
b7892b10ef | ||
|
|
ff5fd43a9e | ||
|
|
34c13db872 | ||
|
+8 |
e044f963f8 | ||
|
|
0344130f68 | ||
|
|
5f78eceb89 | ||
|
|
39a3a1affb | ||
|
|
bc6f959c7a | ||
|
|
bb45b9617b | ||
|
+8 |
8725034b6c | ||
|
|
e53abd921e | ||
|
|
7bb3397503 | ||
|
|
72d19d1d62 | ||
|
|
2931f4f84a | ||
|
|
7756b9c3a1 | ||
|
|
477941545e | ||
|
|
237828e4ed | ||
|
|
e23f3b27bf | ||
|
|
ee084aa8bd | ||
|
|
8ece2525e5 | ||
|
|
865c67a962 | ||
|
|
b6be94fd14 | ||
|
|
f298865d9e | ||
|
|
9e85770435 | ||
|
|
1427c8e792 | ||
|
|
baa641d1f4 | ||
|
|
51e08a7642 | ||
|
|
373c255733 | ||
|
|
f4fb4ddad1 | ||
|
|
79c491986a | ||
|
|
759a89bb0c | ||
|
|
41689ecc92 | ||
|
|
9118ca328f | ||
|
|
8fc4d342fd | ||
|
|
7f2560e1e3 | ||
|
|
3e201c9fe8 | ||
|
|
5ab5af753a | ||
|
|
5c01467caa | ||
|
|
3cf47c2ee9 | ||
|
|
8272fcd5c7 | ||
|
|
95e3f55f26 | ||
|
|
431e4224fe | ||
|
|
59ef4fe459 | ||
|
|
4211ec1a82 | ||
|
|
2e169801e8 | ||
|
|
d0d793a719 | ||
|
|
05f303bfad | ||
|
+9 |
c1b41d8251 | ||
|
|
06732c5c6d | ||
|
|
aa038acafd | ||
|
|
9842c18d50 | ||
|
|
68f05ce2c7 | ||
|
|
1aade4fc57 | ||
|
|
33d71f2390 | ||
|
|
44ef1f80e9 | ||
|
|
9e3e1418c2 | ||
|
|
0284ffc75f | ||
|
|
aee1b3b596 | ||
|
|
c1bc0286d1 | ||
|
|
48d1e95509 | ||
|
|
d8489a662e | ||
|
|
0fc1449dd9 | ||
|
|
d5024d1b70 | ||
|
|
b7161694a5 | ||
|
|
cc7e4b3b33 | ||
|
|
3b9653df0e | ||
|
|
83442caf86 | ||
|
|
e6f912bf0c | ||
|
|
e75f200ff7 | ||
|
|
2d30f673e3 | ||
|
|
2e83c7fbf8 | ||
|
|
1b215e4faf | ||
|
|
49429c87cb | ||
|
|
cf423dda70 | ||
|
|
6ee9de6dd8 | ||
|
|
e54bdb4c46 | ||
|
|
1455a6f603 | ||
|
|
9b980b3b85 | ||
|
|
6420218cb3 | ||
|
|
f9ca763523 | ||
|
|
c4f3dec854 | ||
|
|
ff01da540d | ||
|
|
9021486cd7 | ||
|
|
550f98a636 | ||
|
|
9a4746844f | ||
|
|
271d4319b9 | ||
|
|
215e3c93d6 | ||
|
|
670aed9fe0 | ||
|
|
8583d9b2e6 | ||
|
|
88cad2e15f | ||
|
|
1c165bd934 | ||
|
|
74a16e28f4 | ||
|
|
3e417330b5 | ||
|
|
3af31860bb | ||
|
|
f0aded92db | ||
|
|
a28325e69a | ||
|
|
f949cf1bd1 | ||
|
|
ba31c9dcb5 | ||
|
|
e5af99f572 | ||
|
|
c40ff2e6bb | ||
|
|
b1656c7900 | ||
|
|
a9b18496e0 | ||
|
|
d35ad510c9 | ||
|
+9 |
6e834aa8eb | ||
|
|
5293db29d9 | ||
|
|
e772b4203e | ||
|
|
6cc8fcae52 | ||
|
|
7e5c686e26 | ||
|
|
2c6c768145 | ||
|
|
a5fee47989 | ||
|
|
bceeee70e0 | ||
|
|
baecd923af | ||
|
|
1fe1130af1 | ||
|
|
49805054ba | ||
|
|
e2362d8f2a | ||
|
|
9453c1fc3a | ||
|
|
47e496fded | ||
|
|
0289e727ad | ||
|
|
04b5125121 |
+7
-4
@@ -96,11 +96,10 @@ NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE=
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_PLAN_PRICE=
|
||||
NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRO_PLAN_PRODUCT_ID=
|
||||
STRIPE_PREMIUM_PLAN_PRODUCT_ID=
|
||||
STRIPE_FREE_PLAN_PRODUCT_ID=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
@@ -137,3 +136,7 @@ CLOSECOM_API_KEY=
|
||||
|
||||
# Sendgrid internal email sender
|
||||
SENDGRID_API_KEY=
|
||||
|
||||
# Sentry
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
name: Add PRs to project
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-PR-to-project:
|
||||
name: Add PR to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.1.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/calcom/projects/1
|
||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
@@ -17,6 +17,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.1.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/calcom/projects/1
|
||||
project-url: https://github.com/orgs/calcom/projects/9
|
||||
github-token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
labeled: 🐛 bug, 🎨 needs design, 🙋🏻♂️help wanted, 🚨 urgent
|
||||
labeled: 🙋🏻♂️help wanted
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# .github/workflows/preview.yml
|
||||
|
||||
name: Preview Environment
|
||||
|
||||
env:
|
||||
SNAPLET_ACCESS_TOKEN: ${{ secrets.SNAPLET_ACCESS_TOKEN }}
|
||||
SNAPLET_PROJECT_ID: ${{ secrets.SNAPLET_PROJECT_ID }}
|
||||
VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
db-preview-deploy:
|
||||
if: ${{ github.event.action == 'opened' || github.event.action == 'synchronize' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: snaplet
|
||||
uses: snaplet/action@v1
|
||||
- uses: snaplet/vercel-action@v1
|
||||
with:
|
||||
env: |
|
||||
DATABASE_URL=${{ steps.snaplet.outputs.database-url }}
|
||||
db-preview-delete:
|
||||
if: ${{ github.event.action == 'closed' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: snaplet/action@v1
|
||||
with:
|
||||
delete: true
|
||||
- uses: snaplet/vercel-action@v1
|
||||
with:
|
||||
delete: true
|
||||
@@ -25,6 +25,7 @@ jobs:
|
||||
GOOGLE_LOGIN_ENABLED: true
|
||||
# CRON_API_KEY: xxx
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_FREE_PLAN_PRICE }}
|
||||
NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE }}
|
||||
|
||||
@@ -30,7 +30,10 @@ jobs:
|
||||
if: steps.yarn-cache.outputs.cache-hit != 'true'
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
- name: Test Code Linting
|
||||
run: yarn lint
|
||||
|
||||
- name: Save Code Linting Reports
|
||||
run: yarn lint:report
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
on:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "*" # Push events to every tag not containing /
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: curl -X POST -d {} ${{ secrets.VERCEL_VERSIONED_DEPLOY_HOOK }}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Unit tests
|
||||
on:
|
||||
push:
|
||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||
branches: [ feat/routing-forms/reporting ] # TODO: Remove this after merged in main
|
||||
pull_request_target: # So we can test on forks
|
||||
branches:
|
||||
- main
|
||||
@@ -21,7 +21,8 @@ jobs:
|
||||
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
|
||||
# Should be an 8GB machine as per https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners
|
||||
- run: echo 'NODE_OPTIONS="--max_old_space_size=6144"' >> $GITHUB_ENV
|
||||
- name: Use Node 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"projectId": "cl4u26bwz7962859ply7ibuo43t",
|
||||
"databaseUrl": "postgresql://postgres@localhost:5450/calendso"
|
||||
"targetDatabaseUrl": "postgresql://postgres@localhost:5450/calendso"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "0.3.0"
|
||||
"version": "0.22.3"
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<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://github.com/orgs/calcom/projects/9"><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>
|
||||
@@ -324,7 +324,7 @@ We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/
|
||||
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
||||
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
|
||||
8. Select Web Application as the Application Type.
|
||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
|
||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||
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.
|
||||
|
||||
@@ -354,56 +354,6 @@ following
|
||||
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
|
||||
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
|
||||
|
||||
### Obtaining Slack Client ID and Secret and Signing Secret
|
||||
|
||||
To test this you will need to create a Slack app for yourself on [their apps website](https://api.slack.com/apps).
|
||||
|
||||
Copy and paste the app manifest below into the setting on your slack app. Be sure to replace `YOUR_DOMAIN` with your own domain or your proxy host if you're testing locally.
|
||||
|
||||
<details>
|
||||
<summary>App Manifest</summary>
|
||||
|
||||
```yaml
|
||||
display_information:
|
||||
name: Cal.com Slack
|
||||
features:
|
||||
bot_user:
|
||||
display_name: Cal.com Slack
|
||||
always_online: false
|
||||
slash_commands:
|
||||
- command: /create-event
|
||||
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
|
||||
description: Create an event within Cal!
|
||||
should_escape: false
|
||||
- command: /today
|
||||
url: https://YOUR_DOMAIN/api/integrations/slackmessaging/commandHandler
|
||||
description: View all your bookings for today
|
||||
should_escape: false
|
||||
oauth_config:
|
||||
redirect_urls:
|
||||
- https://YOUR_DOMAIN/api/integrations/slackmessaging/callback
|
||||
scopes:
|
||||
bot:
|
||||
- chat:write
|
||||
- commands
|
||||
- chat:write.public
|
||||
settings:
|
||||
interactivity:
|
||||
is_enabled: true
|
||||
request_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
|
||||
message_menu_options_url: https://YOUR_DOMAIN/api/integrations/slackmessaging/interactiveHandler
|
||||
org_deploy_enabled: false
|
||||
socket_mode_enabled: false
|
||||
token_rotation_enabled: false
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Add the integration as normal - slack app - add. Follow the oauth flow to add it to a server.
|
||||
|
||||
Next make sure you have your app running `yarn dx`. Then in the slack chat type one of these commands: `/create-event` or `/today`
|
||||
|
||||
> NOTE: Next you will need to setup a proxy server like [ngrok](https://ngrok.com/) to allow your local host machine to be hosted on a public https server.
|
||||
|
||||
### Obtaining Zoom Client ID and Secret
|
||||
|
||||
|
||||
+1
-1
Submodule apps/api updated: 7e9226fabc...0f5017010f
+1
-1
Submodule apps/console updated: 995771ebbd...2219900e06
@@ -17,7 +17,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iframe-resizer-react": "^1.1.0",
|
||||
"next": "^12.2.5",
|
||||
"next": "^12.3.1",
|
||||
"nextra": "^1.1.0",
|
||||
"nextra-theme-docs": "^1.2.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
Executable → Regular
+19
-33
@@ -1,38 +1,24 @@
|
||||
# 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/
|
||||
next-env.d.ts
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
storybook-static/
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources: [],
|
||||
debug: true,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -1,34 +1,29 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
stories: [
|
||||
"../intro.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.mdx",
|
||||
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"storybook-addon-designs",
|
||||
"@storybook/addon-a11y",
|
||||
"storybook-addon-next",
|
||||
"storybook-react-i18next",
|
||||
{
|
||||
name: "storybook-addon-next",
|
||||
options: {
|
||||
nextConfigPath: path.resolve(__dirname, "../../web/next.config.js"),
|
||||
},
|
||||
},
|
||||
],
|
||||
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"];
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
"@/interfaces": path.resolve(__dirname, "../interfaces"),
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}}
|
||||
*/
|
||||
staticDirs: ["../public"],
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
config.resolve.fallback = {
|
||||
fs: false,
|
||||
assert: false,
|
||||
@@ -55,7 +50,6 @@ module.exports = {
|
||||
vm: false,
|
||||
zlib: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
window.isEmbed = ()=> {
|
||||
return location.search.includes("embed=")
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { RouterContext } from "next/dist/shared/lib/router-context";
|
||||
import * as NextImage from "next/image";
|
||||
import { addDecorator } from "@storybook/react";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
const OriginalNextImage = NextImage.default;
|
||||
|
||||
Object.defineProperty(NextImage, "default", {
|
||||
configurable: true,
|
||||
value: (props) => <OriginalNextImage {...props} unoptimized />,
|
||||
});
|
||||
import "../styles/storybook-styles.css";
|
||||
import i18n from "./i18next";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
@@ -18,11 +13,10 @@ export const parameters = {
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
},
|
||||
};
|
||||
|
||||
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ArgsTable } from "@storybook/addon-docs";
|
||||
import { SortType } from "@storybook/components";
|
||||
import { PropDescriptor } from "@storybook/store";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
|
||||
type Component = any;
|
||||
type BaseProps = {
|
||||
include?: PropDescriptor;
|
||||
exclude?: PropDescriptor;
|
||||
sort?: SortType;
|
||||
};
|
||||
|
||||
type OfProps = BaseProps & {
|
||||
of: "." | "^" | Component;
|
||||
};
|
||||
|
||||
export function CustomArgsTable({ of, sort }: OfProps) {
|
||||
return (
|
||||
<div className="custom-args-wrapper">
|
||||
<ArgsTable of={of} sort={sort} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
interface ExampleProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
isFullWidth?: boolean;
|
||||
}
|
||||
export const Example = ({ children, title, isFullWidth = false }: ExampleProps) => {
|
||||
return (
|
||||
<div className={classNames("examples-item", isFullWidth && "w-full")}>
|
||||
<span className={classNames("examples-item-title", isFullWidth && "mb-0 mt-2")}>{title}</span>
|
||||
<div className="examples-item-content">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExamplesProps {
|
||||
children: React.ReactNode;
|
||||
title: string;
|
||||
footnote?: React.ReactNode;
|
||||
dark?: boolean;
|
||||
}
|
||||
|
||||
export const Examples = ({ children, title, footnote = null, dark }: ExamplesProps) => {
|
||||
return (
|
||||
<div className={classNames("examples", dark && "dark")}>
|
||||
<h2 className="examples-title">{title}</h2>
|
||||
<div className="examples-content">{children}</div>
|
||||
{!!footnote && <div className="examples-footnote">{footnote}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export const Note = ({ children }: { children: React.ReactNode }) => (
|
||||
<div className="story-note">
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
export const Title = ({
|
||||
title,
|
||||
suffix,
|
||||
subtitle,
|
||||
offset,
|
||||
}: {
|
||||
title: string;
|
||||
suffix?: string;
|
||||
subtitle?: string;
|
||||
offset?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`story-title ${offset && "offset"}`}>
|
||||
<h1>
|
||||
{title}
|
||||
{suffix && <span>{suffix}</span>}
|
||||
</h1>
|
||||
{subtitle && <p>{subtitle}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { ReactElement, ReactNode } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
export function VariantsTable({
|
||||
children,
|
||||
titles,
|
||||
isDark,
|
||||
columnMinWidth = 150,
|
||||
}: {
|
||||
children: ReactElement<RowProps> | ReactElement<RowProps>[];
|
||||
titles: string[];
|
||||
isDark?: boolean;
|
||||
// Mainly useful on mobile, so components don't get squeesed
|
||||
columnMinWidth?: number;
|
||||
}) {
|
||||
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
isDark &&
|
||||
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#22252A]"
|
||||
)}>
|
||||
<div className="z-1 relative mx-auto w-full max-w-[1200px] overflow-auto pr-8 pt-6">
|
||||
<table>
|
||||
<RowTitles titles={titles} />
|
||||
{columns.map((column) => (
|
||||
<tr className="p-2 pr-6 pb-6" key={column.props.variant}>
|
||||
<th
|
||||
className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]"
|
||||
key={column.props.variant}>
|
||||
{column.props.variant}
|
||||
</th>
|
||||
{React.Children.count(column.props.children) &&
|
||||
React.Children.map(column.props.children, (cell) => (
|
||||
<td className="p-2 pr-6 pb-6" style={{ minWidth: `${columnMinWidth}px` }}>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
{!isDark && (
|
||||
<div data-mode="dark">
|
||||
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
|
||||
{children}
|
||||
</VariantsTable>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RowProps {
|
||||
variant: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* There are two reasons we have this "empty" wrapper component:
|
||||
* 1. In order to have an isolate group per variant, which we iterate through in the table component.
|
||||
* 2. To have a way to pass the variant.
|
||||
*/
|
||||
export function VariantRow({ children }: RowProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export function RowTitles({ titles }: { titles: string[] }) {
|
||||
return (
|
||||
<tr>
|
||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" />
|
||||
{titles.map((title) => (
|
||||
<th className="p-2 pr-6 pb-6 text-left text-sm font-normal text-[#8F8F8F]" key={title}>
|
||||
{title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./Examples";
|
||||
export * from "./Note";
|
||||
export * from "./VariantsTable";
|
||||
export * from "./Title";
|
||||
export * from "./CustomArgsTable";
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Meta } from '@storybook/addon-docs';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
<div className="text-center flex flex-col items-center">
|
||||
<h1 style={{marginBottom: '24px', marginTop: '36px'}}>Welcome to Cal.com UI</h1>
|
||||
<p>This is the beginning of our storybook impovments to match Figma as close as possible. Like our Figma library, we will be adding more components as we go along.</p>
|
||||
<p>Our <a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">Figma</a> library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to the design team.</p>
|
||||
</div>
|
||||
|
||||
<img src="/sb-cover.jpg" />
|
||||
@@ -1,7 +0,0 @@
|
||||
const withTM = require("next-transpile-modules")(["@calcom/dayjs", "@calcom/ui"]);
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = withTM(nextConfig);
|
||||
+28
-40
@@ -1,18 +1,13 @@
|
||||
{
|
||||
"name": "@calcom/storybook",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"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"
|
||||
"build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/config": "*",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
@@ -25,42 +20,35 @@
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"next": "^12.2.5",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"next": "^12.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-hot-toast": "^2.3.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.10",
|
||||
"@calcom/config": "*",
|
||||
"@storybook/addon-a11y": "^6.5.10",
|
||||
"@storybook/addon-actions": "^6.5.10",
|
||||
"@storybook/addon-essentials": "^6.5.10",
|
||||
"@storybook/addon-interactions": "^6.5.10",
|
||||
"@storybook/addon-links": "^6.5.10",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-vite": "^0.2.2",
|
||||
"@storybook/builder-webpack5": "^6.5.10",
|
||||
"@storybook/manager-webpack5": "^6.5.10",
|
||||
"@storybook/react": "^6.5.10",
|
||||
"@tailwindcss/line-clamp": "^0.4.0",
|
||||
"@types/node": "16.9.1",
|
||||
"@babel/core": "^7.19.6",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/builder-vite": "^0.2.4",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "18.0.4",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"babel-loader": "^8.2.5",
|
||||
"chromatic": "^6.7.4",
|
||||
"eslint": "^8.22.0",
|
||||
"postcss": "^8.4.16",
|
||||
"postcss-loader": "^7.0.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-pseudo-companion-classes": "^0.1.1",
|
||||
"rollup-plugin-polyfill-node": "^0.10.2",
|
||||
"storybook-addon-designs": "^6.3.1",
|
||||
"storybook-addon-next": "^1.6.7",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"_id": "@calcom/storybook@0.1.0"
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { AppProps } from "next/app";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
@@ -1,63 +0,0 @@
|
||||
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;
|
||||
@@ -2,5 +2,21 @@ module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
/**
|
||||
* We use this postcss plugin to create custom css classes for
|
||||
* any pseudo classes (eg hover, focus) mentioned below.
|
||||
* This way you can eg show a hover state in Storybook by adding
|
||||
* this classname .sb-pseudo--hover to the item.
|
||||
*
|
||||
* These styles will only be added in storybook, and will NOT
|
||||
* end up in the final css bundle of apps using the components.
|
||||
*/
|
||||
"postcss-pseudo-companion-classes": {
|
||||
prefix: "sb-pseudo--",
|
||||
// We have to keep a restrictTo list here, because otherwise
|
||||
// this library will have issues processing tailwind's \: prefixed classes
|
||||
// and start adding dots everywhere, breaking this functionality.
|
||||
restrictTo: [":hover", ":focus"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
@@ -1,4 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,26 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import Avatar from "@calcom/ui/v2/core/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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import AvatarGroup from "@calcom/ui/v2/core/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 (
|
||||
<TooltipProvider>
|
||||
<AvatarGroup size="lg" items={IMAGES} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Bell } from "react-feather";
|
||||
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -1,46 +0,0 @@
|
||||
import { ComponentStory, ComponentMeta } from "@storybook/react";
|
||||
import { Info } from "react-feather";
|
||||
|
||||
import Banner from "@calcom/ui/v2/core/banner";
|
||||
|
||||
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")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import Breadcrumb, { BreadcrumbItem } from "@calcom/ui/v2/core/Breadcrumb";
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import { Trash2 } from "react-feather";
|
||||
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
export default {
|
||||
title: "Button",
|
||||
component: Button,
|
||||
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 Button>;
|
||||
|
||||
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;
|
||||
|
||||
export const All = () => (
|
||||
<div>
|
||||
<TooltipProvider>
|
||||
<h1>Primary</h1>
|
||||
<div className="flex space-x-2">
|
||||
<Button aria-label="Button Text">Button Text</Button>
|
||||
<Button disabled aria-label="Button Text">
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
<h1>Secondary</h1>
|
||||
<div className="flex space-x-2">
|
||||
<Button color="secondary" aria-label="Button Text">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button disabled color="secondary" aria-label="Button Text">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="icon" color="secondary" StartIcon={Trash2} aria-label="Button Text" />
|
||||
</div>
|
||||
<h1>Minimal</h1>
|
||||
<div className="flex">
|
||||
<Button color="minimal" aria-label="Button Text">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button disabled color="minimal" aria-label="Button Text">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="icon" color="minimal" StartIcon={Trash2} aria-label="Button Text" />
|
||||
</div>
|
||||
<h1>Destructive</h1>
|
||||
<Button size="icon" color="destructive" StartIcon={Trash2} aria-label="Button Text" />
|
||||
<h1>Tooltip</h1>
|
||||
<Button
|
||||
tooltip="Deletes EventTypes"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
StartIcon={Trash2}
|
||||
aria-label="Button Text"
|
||||
/>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
color: "primary",
|
||||
children: "Button Text",
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Default.args,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
export const Loading = Template.bind({});
|
||||
Loading.args = {
|
||||
...Default.args,
|
||||
loading: true,
|
||||
};
|
||||
export const Icon = Template.bind({});
|
||||
Icon.args = {
|
||||
color: "secondary",
|
||||
StartIcon: Trash2,
|
||||
size: "icon",
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { ArrowLeft, ArrowRight, Clipboard, Navigation2, Trash2 } from "react-feather";
|
||||
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import ButtonGroup from "@calcom/ui/v2/core/ButtonGroup";
|
||||
|
||||
export default {
|
||||
title: "Button Group",
|
||||
component: ButtonGroup,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<TooltipProvider>
|
||||
<Story />
|
||||
</TooltipProvider>
|
||||
),
|
||||
],
|
||||
} 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" />
|
||||
<Button StartIcon={Navigation2} size="icon" color="secondary" />
|
||||
<Button StartIcon={Clipboard} size="icon" color="secondary" />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup combined>
|
||||
<Button StartIcon={ArrowLeft} size="icon" color="secondary" />
|
||||
<Button StartIcon={ArrowRight} size="icon" color="secondary" />
|
||||
<Button StartIcon={ArrowRight} href="/" size="icon" color="secondary" />
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
@@ -1,79 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import Card, { BaseCardProps } from "@calcom/ui/v2/core/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>
|
||||
);
|
||||
};
|
||||
|
||||
const sidebarCardProps: BaseCardProps = {
|
||||
variant: "SidebarCard",
|
||||
thumbnailUrl: "https://img.youtube.com/vi/60HJt8DOVNo/0.jpg",
|
||||
mediaLink: "https://www.youtube.com/watch?v=60HJt8DOVNo",
|
||||
title: "Dynamic boooking links",
|
||||
description: "Booking link that allows people to quickly schedule meetings.",
|
||||
learnMore: {
|
||||
href: "https://cal.com/blog/cal-v-1-9",
|
||||
text: "Learn more",
|
||||
},
|
||||
};
|
||||
|
||||
export const SidebarCard = () => {
|
||||
const [visible, setVisible] = useState(true); // save state in localStorage, cookie or db
|
||||
return (
|
||||
<header className="w-full max-w-[225px] bg-gray-100 p-3">
|
||||
{visible && (
|
||||
<div>
|
||||
<Card {...sidebarCardProps} actionButton={{ onClick: () => setVisible(false), child: "Dismiss" }} />
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import Checkbox from "@calcom/ui/v2/core/form/Checkbox";
|
||||
|
||||
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 />
|
||||
</>
|
||||
);
|
||||
@@ -1,208 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import ColorPicker from "@calcom/ui/v2/core/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)} />;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||
|
||||
export default {
|
||||
title: "Datepicker",
|
||||
component: DatePicker,
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
const [selected, setSelected] = useState<Date>(new Date());
|
||||
return (
|
||||
<div style={{ width: "455px" }}>
|
||||
<DatePicker selected={selected} onChange={setSelected} locale="en" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import "@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
||||
import { useState } from "react";
|
||||
|
||||
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
||||
import "@calcom/ui/v2/core/form/date-range-picker/styles.css";
|
||||
|
||||
function getISOLocalDate(date) {
|
||||
const year = padStart(date.getFullYear(), 4);
|
||||
const month = padStart(date.getMonth() + 1);
|
||||
const day = padStart(date.getDate());
|
||||
|
||||
return [`${year}-${month}-${day}`];
|
||||
}
|
||||
|
||||
function padStart(num, val = 2) {
|
||||
const numStr = `${num}`;
|
||||
if (numStr.length >= val) {
|
||||
return num;
|
||||
}
|
||||
|
||||
return `0000${numStr}`.slice(-val);
|
||||
}
|
||||
|
||||
export default {
|
||||
title: "Date Range Picker",
|
||||
component: DateRangePicker,
|
||||
} as ComponentMeta<typeof DateRangePicker>;
|
||||
|
||||
export const Default: ComponentStory<typeof DateRangePicker> = () => {
|
||||
const [startDate, setStartDate] = useState<Date>(new Date());
|
||||
const [endDate, setEndDate] = useState<Date>(new Date());
|
||||
return (
|
||||
<div>
|
||||
<DateRangePicker
|
||||
startDate={getISOLocalDate(startDate) as unknown as Date}
|
||||
endDate={getISOLocalDate(endDate) as unknown as Date}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
setStartDate(startDate);
|
||||
setEndDate(endDate);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Bell } from "react-feather";
|
||||
|
||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||
|
||||
export default {
|
||||
title: "pattern/Empty Screen",
|
||||
component: EmptyScreen,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="flex items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} as ComponentMeta<typeof EmptyScreen>;
|
||||
|
||||
export const Default = () => (
|
||||
<EmptyScreen
|
||||
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 "
|
||||
buttonText="Veniam ut ipsum"
|
||||
buttonOnClick={() => console.log("Button Clicked")}
|
||||
/>
|
||||
);
|
||||
@@ -1,27 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import FormStep from "@calcom/ui/v2/core/form/FormStep";
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
// Disabling until we figure out what is happening with the RouterMock.
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem";
|
||||
import HorizontalTabs from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabs";
|
||||
|
||||
export default {
|
||||
title: "Horizontal Tabs",
|
||||
component: HorizontalTabs,
|
||||
} as ComponentMeta<typeof HorizontalTabs>;
|
||||
|
||||
const HorizontalTabsPropsDefault: HorizontalTabItemProps[] = [
|
||||
{
|
||||
name: "Tab One",
|
||||
href: "/tab-one",
|
||||
},
|
||||
{
|
||||
name: "Tab Two",
|
||||
href: "/tab-two",
|
||||
},
|
||||
{
|
||||
name: "Tab Disabled",
|
||||
href: "/tab-disabled",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const Default = () => (
|
||||
<div className="w-full p-4">
|
||||
<HorizontalTabs tabs={HorizontalTabsPropsDefault} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Default.parameters = {
|
||||
nextRouter: {
|
||||
path: "/[page]",
|
||||
asPath: "/tab-one",
|
||||
},
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import { Copy } from "react-feather";
|
||||
|
||||
import { TextAreaField, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields";
|
||||
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||
|
||||
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()} />;
|
||||
|
||||
export const PasswordInput: ComponentStory<typeof PasswordField> = () => (
|
||||
<TooltipProvider>
|
||||
<PasswordField />
|
||||
</TooltipProvider>
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Info } from "react-feather";
|
||||
|
||||
import { Button, Dialog, DialogContent, DialogTrigger, TextField } from "@calcom/ui/v2";
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { Button, showToast } from "@calcom/ui/v2";
|
||||
|
||||
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>
|
||||
);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
import { Search } from "react-feather";
|
||||
|
||||
import { Button, TextField } from "@calcom/ui/v2";
|
||||
import PageHeader from "@calcom/ui/v2/core/PageHeader";
|
||||
|
||||
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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as Radio from "@calcom/ui/v2/core/form/radio-area/Radio";
|
||||
import { RadioField } from "@calcom/ui/v2/core/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>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ComponentMeta } from "@storybook/react";
|
||||
|
||||
import { Select } from "@calcom/ui/v2";
|
||||
import MultiDropdownSelect from "@calcom/ui/v2/modules/event-types/MultiDropdownSelect";
|
||||
|
||||
export default {
|
||||
title: "Form/Select",
|
||||
component: Select,
|
||||
} as ComponentMeta<typeof Select>;
|
||||
|
||||
const singleOptions = [
|
||||
{
|
||||
label: "Select",
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + "minutes",
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
const multiOptions = [
|
||||
{
|
||||
label: "Select",
|
||||
value: 0,
|
||||
},
|
||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||
label: minutes + " " + "minutes",
|
||||
value: minutes,
|
||||
})),
|
||||
];
|
||||
|
||||
export const Single = () => <Select options={singleOptions} />;
|
||||
export const Multi = () => <MultiDropdownSelect options={multiOptions} />;
|
||||
@@ -1,15 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@@ -1,67 +0,0 @@
|
||||
// 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} />
|
||||
</div>
|
||||
);
|
||||
export const Active = () => (
|
||||
<div className="h-20 w-full bg-gray-100 p-4">
|
||||
<VerticalTabItem {...TabItemProps} />
|
||||
</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 />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const NoIconNoInfo = () => (
|
||||
<div className="h-20 w-full bg-gray-100 p-4">
|
||||
<VerticalTabItem name="Event Setup" href="/settings/event" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IconNoInfo = () => (
|
||||
<div className="h-20 w-full bg-gray-100 p-4">
|
||||
<VerticalTabItem name="Event Setup" href="/settings/event" icon={Link} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const IconNoInfoActive = () => (
|
||||
<div className="h-20 w-full bg-gray-100 p-4">
|
||||
<VerticalTabItem name="Event Setup" href="/settings/events" icon={Link} />
|
||||
</div>
|
||||
);
|
||||
|
||||
IconNoInfoActive.paramaters = {
|
||||
nextRouter: {
|
||||
path: "/settings/[tab]",
|
||||
asPath: "/settings/events",
|
||||
},
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
// 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/core/navigation/tabs/VerticalTabs";
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
Executable → Regular
+1
-10
@@ -2,13 +2,4 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import "../../../packages/ui/v2/core/form/date-range-picker/styles.css";
|
||||
@import "../../../node_modules/@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css";
|
||||
|
||||
:root {
|
||||
--brand-color: #292929;
|
||||
--brand-text-color: #ffffff;
|
||||
--brand-color-dark-mode: #fafafa;
|
||||
--brand-text-color-dark-mode: #292929;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@import "../../../packages/ui/styles/shared-globals.css"
|
||||
@@ -0,0 +1,189 @@
|
||||
.sbdocs {
|
||||
font-family: 'Inter var' !important;
|
||||
padding: 0!important;
|
||||
}
|
||||
|
||||
#docs-root {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-h1 {
|
||||
font-family: "Cal Sans", "sans-serif" !important;
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
margin: 0 0 82px 0;
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-top: 2px solid rgba(0, 0, 0, 0.12);
|
||||
padding-top: 12px!important;
|
||||
padding-bottom: 12px!important;
|
||||
margin: 82px 0 0 0;
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-p {
|
||||
max-width: 560px;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-content {
|
||||
width: 1200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/** Docs table **/
|
||||
.custom-args-wrapper{
|
||||
max-height: 600px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.docblock-argstable-body {
|
||||
box-shadow: none!important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* .docblock-argstable-head tr th, */
|
||||
.docblock-argstable-body tr td {
|
||||
/* padding-left: 0!important; */
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
/** Column titles **/
|
||||
.docblock-argstable-body tr td > span,
|
||||
.docblock-argstable-head tr th > span {
|
||||
color: #525252 !important;
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.docblock-argstable-body div p,
|
||||
.docblock-argstable-body div span {
|
||||
color: #8F8F8F !important;
|
||||
}
|
||||
|
||||
/** Custom components **/
|
||||
.story-title {
|
||||
margin: 0 0 82px 0;
|
||||
}
|
||||
|
||||
.story-title.offset {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.story-title h1 {
|
||||
font-family: "Cal Sans", "sans-serif" !important;
|
||||
font-weight: 600;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.story-title h1 span {
|
||||
color: #9CA3AF;
|
||||
font-family: 'Inter var';
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.story-title p {
|
||||
color: #111827;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.examples {
|
||||
background-color: #F9FAFB;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.examples-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.examples-title {
|
||||
color: #8F8F8F;
|
||||
font-size: 14px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.examples-item-title {
|
||||
font-size: 12px;
|
||||
color: #8F8F8F;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.examples-item {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.examples-footnote p,
|
||||
.examples-footnote ul,
|
||||
.examples-footnote li {
|
||||
color: #8F8F8F;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.examples-footnote ul {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.examples-footnote li {
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
.story-note {
|
||||
background-color: #F9FAFB;
|
||||
font-size: 14px;
|
||||
padding: 20px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.story-note > div {
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/** SB Docs Preview **/
|
||||
.sbdocs-preview {
|
||||
width: 100vw;
|
||||
left: calc((100vw - 1200px) / -2);
|
||||
box-shadow: none !important;
|
||||
margin: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: none!important;
|
||||
}
|
||||
|
||||
.docs-story > div:first-child {
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
.docs-story .innerZoomElementWrapper > div {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.sb-main-padded {
|
||||
padding: 0!important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.sbdocs-preview {
|
||||
left: -24px;
|
||||
width: calc(100vw + 48px);
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.sbdocs.sbdocs-content {
|
||||
padding: 24px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,7 @@
|
||||
const base = require("@calcom/config/tailwind-preset");
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
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",
|
||||
}),
|
||||
},
|
||||
},
|
||||
...base,
|
||||
darkMode: ["class", '[data-mode="dark"]'],
|
||||
content: [...base.content, "../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}", "./stories/**/*.{js,ts,tsx,jsx}"],
|
||||
};
|
||||
|
||||
Executable → Regular
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "^12.2.5",
|
||||
"next": "^12.3.1",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "18.0.4",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -67,4 +67,6 @@ tsconfig.tsbuildinfo
|
||||
public/embed
|
||||
|
||||
# Copied app-store images
|
||||
public/app-store
|
||||
public/app-store
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
@@ -598,60 +598,6 @@ paths:
|
||||
summary: Gets and stores the OAuth token
|
||||
tags:
|
||||
- Integrations
|
||||
/api/user/profile:
|
||||
patch:
|
||||
description: Updates a user's profile.
|
||||
summary: Updates a user's profile
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
avatar:
|
||||
type: string
|
||||
timeZone:
|
||||
type: string
|
||||
weekStart:
|
||||
type: string
|
||||
hideBranding:
|
||||
type: string
|
||||
theme:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
/api/me:
|
||||
get:
|
||||
description: Gets current user's profile.
|
||||
@@ -686,71 +632,6 @@ paths:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
/api/user/membership:
|
||||
get:
|
||||
description: Get a list of the teams the user has joined.
|
||||
summary: Get the teams a user is joined to
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
patch:
|
||||
description: Accept team invitation
|
||||
summary: Accept team invitation.
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
teamId:
|
||||
type: string
|
||||
delete:
|
||||
description: Leave team or decline membership invite of current user
|
||||
summary: Leave team or decline team invite.
|
||||
tags:
|
||||
- User
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
teamId:
|
||||
type: string
|
||||
"/api/{team}":
|
||||
delete:
|
||||
description: Deletes a team
|
||||
|
||||
@@ -36,7 +36,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
||||
|
||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: true }]);
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { 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 { Icon } from "@calcom/ui/Icon";
|
||||
import Shell from "@calcom/ui/Shell";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
logo,
|
||||
body,
|
||||
categories,
|
||||
author,
|
||||
price = 0,
|
||||
commission,
|
||||
isGlobal = false,
|
||||
feeType,
|
||||
docs,
|
||||
website,
|
||||
email,
|
||||
tos,
|
||||
privacy,
|
||||
isProOnly,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const handleOpenChange = () => {
|
||||
utils.invalidateQueries(["viewer.integrations"]);
|
||||
router.replace("/apps/installed");
|
||||
};
|
||||
|
||||
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",
|
||||
currency: "USD",
|
||||
useGrouping: false,
|
||||
}).format(price);
|
||||
|
||||
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||
const appCredentials = trpc.useQuery(["viewer.appCredentialsByType", { appType: type }], {
|
||||
onSuccess(data) {
|
||||
setExistingCredentials(data);
|
||||
},
|
||||
});
|
||||
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||
|
||||
return (
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="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>
|
||||
</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">
|
||||
{!appCredentials.isLoading ? (
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("globally_install")}
|
||||
</Button>
|
||||
{!isGlobal && (
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : existingCredentials.length > 0 ? (
|
||||
<DisconnectIntegration
|
||||
id={existingCredentials[0]}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn" data-testid={type + "-integration-disconnect-button"}>
|
||||
{t("disconnect")}
|
||||
</Button>
|
||||
)}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ const DestinationCalendarSelector = ({
|
||||
destinationCalendar,
|
||||
}: Props): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
||||
|
||||
// Extra styles to show prefixed text in react-select
|
||||
|
||||
+215
-209
@@ -6,8 +6,9 @@ import { components, ControlProps } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Button } from "@calcom/ui/components";
|
||||
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
|
||||
import { Button, HorizontalTabs, showToast, Switch } from "@calcom/ui/v2";
|
||||
import { HorizontalTabs, showToast, Switch } from "@calcom/ui/v2";
|
||||
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/v2/core/Dialog";
|
||||
|
||||
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
|
||||
@@ -56,10 +57,13 @@ const goto = (router: NextRouter, searchParams: Record<string, string>) => {
|
||||
};
|
||||
|
||||
const removeQueryParams = (router: NextRouter, queryParams: string[]) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
queryParams.forEach((param) => {
|
||||
delete router.query[param];
|
||||
params.delete(param);
|
||||
});
|
||||
router.push(`${router.asPath.split("?")[0]}?${router.query.toString()}`);
|
||||
|
||||
router.push(`${router.asPath.split("?")[0]}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -797,245 +801,247 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
{embed.title}
|
||||
</h3>
|
||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
className="flex w-full items-center text-base font-medium text-neutral-900">
|
||||
<div>
|
||||
{embedType === "inline"
|
||||
? "Inline Embed Customization"
|
||||
: embedType === "floating-popup"
|
||||
? "Floating Popup Customization"
|
||||
: "Element Click Customization"}
|
||||
</div>
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="text-sm">
|
||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||
<div className="text-sm">Embed Window Sizing</div>
|
||||
<div className="justify-left flex items-center">
|
||||
<TextField
|
||||
name="width"
|
||||
labelProps={{ className: "hidden" }}
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
const width = e.target.value || "100%";
|
||||
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
width,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>W</InputLeading>}
|
||||
/>
|
||||
<span className="p-2">×</span>
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
name="height"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
onChange={(e) => {
|
||||
const height = e.target.value || "100%";
|
||||
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
height,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>H</InputLeading>}
|
||||
/>
|
||||
<div className="max-h-97 flex flex-col overflow-y-auto">
|
||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
className="flex w-full items-center text-base font-medium text-neutral-900">
|
||||
<div>
|
||||
{embedType === "inline"
|
||||
? "Inline Embed Customization"
|
||||
: embedType === "floating-popup"
|
||||
? "Floating Popup Customization"
|
||||
: "Element Click Customization"}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 items-center justify-between",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}>
|
||||
<div className="mb-2 text-sm">Button Text</div>
|
||||
{/* Default Values should come from preview iframe */}
|
||||
<TextField
|
||||
name="buttonText"
|
||||
labelProps={{ className: "hidden" }}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonText: e.target.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
defaultValue="Book my Cal"
|
||||
required
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 flex items-center justify-start",
|
||||
embedType === "floating-popup" ? "space-x-2" : "hidden"
|
||||
)}>
|
||||
<Switch
|
||||
defaultChecked={true}
|
||||
onCheckedChange={(checked) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
hideButtonIcon: !checked,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm">Display Calendar Icon Button</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 items-center justify-between",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}>
|
||||
<div className="mb-2">Position of Button</div>
|
||||
<Select
|
||||
onChange={(position) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonPosition: position?.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
defaultValue={FloatingPopupPositionOptions[0]}
|
||||
options={FloatingPopupPositionOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div>Button Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="text-sm">
|
||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||
<div className="text-sm">Embed Window Sizing</div>
|
||||
<div className="justify-left flex items-center">
|
||||
<TextField
|
||||
name="width"
|
||||
labelProps={{ className: "hidden" }}
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
const width = e.target.value || "100%";
|
||||
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
width,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>W</InputLeading>}
|
||||
/>
|
||||
<span className="p-2">×</span>
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
name="height"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
onChange={(e) => {
|
||||
const height = e.target.value || "100%";
|
||||
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
inline: {
|
||||
...previewState.inline,
|
||||
height,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>H</InputLeading>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 items-center justify-between",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}>
|
||||
<div className="mb-2 text-sm">Button Text</div>
|
||||
{/* Default Values should come from preview iframe */}
|
||||
<TextField
|
||||
name="buttonText"
|
||||
labelProps={{ className: "hidden" }}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonColor: color,
|
||||
buttonText: e.target.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
defaultValue="Book my Cal"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div>Text Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 flex items-center justify-start",
|
||||
embedType === "floating-popup" ? "space-x-2" : "hidden"
|
||||
)}>
|
||||
<Switch
|
||||
defaultChecked={true}
|
||||
onCheckedChange={(checked) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonTextColor: color,
|
||||
hideButtonIcon: !checked,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm">Display Calendar Icon Button</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<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>
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-6 text-sm">
|
||||
<Label className="">
|
||||
<div className="mb-2">Theme</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 items-center justify-between",
|
||||
embedType === "floating-popup" ? "" : "hidden"
|
||||
)}>
|
||||
<div className="mb-2">Position of Button</div>
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={ThemeOptions[0]}
|
||||
components={{
|
||||
Control: ThemeSelectControl,
|
||||
}}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
onChange={(position) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
theme: option.value,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonPosition: position?.value,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={ThemeOptions}
|
||||
defaultValue={FloatingPopupPositionOptions[0]}
|
||||
options={FloatingPopupPositionOptions}
|
||||
/>
|
||||
</Label>
|
||||
{[
|
||||
{ name: "brandColor", title: "Brand Color" },
|
||||
// { name: "lightColor", title: "Light Color" },
|
||||
// { name: "lighterColor", title: "Lighter Color" },
|
||||
// { name: "lightestColor", title: "Lightest Color" },
|
||||
// { name: "highlightColor", title: "Highlight Color" },
|
||||
// { name: "medianColor", title: "Median Color" },
|
||||
].map((palette) => (
|
||||
<Label key={palette.name} className="pb-4">
|
||||
<div className="mb-2 pt-2">{palette.title}</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
addToPalette({
|
||||
[palette.name as keyof typeof previewState["palette"]]: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div>Button Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonColor: color,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames("mt-4", embedType === "floating-popup" ? "" : "hidden")}>
|
||||
<div>Text Color</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
floatingPopup: {
|
||||
...previewState.floatingPopup,
|
||||
buttonTextColor: color,
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<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>
|
||||
<Icon.FiChevronRight
|
||||
className={`${
|
||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-neutral-500`}
|
||||
/>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="mt-6 text-sm">
|
||||
<Label className="">
|
||||
<div className="mb-2">Theme</div>
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={ThemeOptions[0]}
|
||||
components={{
|
||||
Control: ThemeSelectControl,
|
||||
}}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
setPreviewState((previewState) => {
|
||||
return {
|
||||
...previewState,
|
||||
theme: option.value,
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={ThemeOptions}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
{[
|
||||
{ name: "brandColor", title: "Brand Color" },
|
||||
// { name: "lightColor", title: "Light Color" },
|
||||
// { name: "lighterColor", title: "Lighter Color" },
|
||||
// { name: "lightestColor", title: "Lightest Color" },
|
||||
// { name: "highlightColor", title: "Highlight Color" },
|
||||
// { name: "medianColor", title: "Median Color" },
|
||||
].map((palette) => (
|
||||
<Label key={palette.name} className="pb-4">
|
||||
<div className="mb-2 pt-2">{palette.title}</div>
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
defaultValue="#000000"
|
||||
onChange={(color) => {
|
||||
addToPalette({
|
||||
[palette.name as keyof typeof previewState["palette"]]: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-2/3 flex-col p-8">
|
||||
|
||||
@@ -11,14 +11,14 @@ export type GateState = {
|
||||
type GateProps = {
|
||||
children: React.ReactNode;
|
||||
gates: Gate[];
|
||||
metadata: JSONObject;
|
||||
appData: JSONObject;
|
||||
dispatch: Dispatch<Partial<GateState>>;
|
||||
};
|
||||
|
||||
const RainbowGate = dynamic(() => import("@calcom/app-store/rainbow/components/RainbowKit"));
|
||||
|
||||
// To add a new Gate just add the gate logic to the switch statement
|
||||
const Gates: React.FC<GateProps> = ({ children, gates, metadata, dispatch }) => {
|
||||
const Gates: React.FC<GateProps> = ({ children, gates, appData, dispatch }) => {
|
||||
const [rainbowToken, setRainbowToken] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,12 +31,12 @@ const Gates: React.FC<GateProps> = ({ children, gates, metadata, dispatch }) =>
|
||||
for (const gate of gates) {
|
||||
switch (gate) {
|
||||
case "rainbow":
|
||||
if (metadata.blockchainId && metadata.smartContractAddress && !rainbowToken) {
|
||||
if (appData.blockchainId && appData.smartContractAddress && !rainbowToken) {
|
||||
gateWrappers = (
|
||||
<RainbowGate
|
||||
setToken={setRainbowToken}
|
||||
chainId={metadata.blockchainId as number}
|
||||
tokenAddress={metadata.smartContractAddress as string}>
|
||||
chainId={appData.blockchainId as number}
|
||||
tokenAddress={appData.smartContractAddress as string}>
|
||||
{gateWrappers}
|
||||
</RainbowGate>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect } from "react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export function useViewerI18n() {
|
||||
return trpc.useQuery(["viewer.public.i18n"], {
|
||||
return trpc.viewer.public.i18n.useQuery(undefined, {
|
||||
staleTime: Infinity,
|
||||
/**
|
||||
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
|
||||
|
||||
+39
-5
@@ -23,11 +23,11 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
||||
type={data.type}
|
||||
render={(installProps) => {
|
||||
return (
|
||||
<Button {...installProps} className="w-full" color="minimal">
|
||||
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
||||
{data.image && (
|
||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
||||
)}
|
||||
<p>{data.label}</p>
|
||||
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
@@ -35,14 +35,14 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
||||
) : (
|
||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
||||
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||
<p>{t("add_new_calendar")}...</p>
|
||||
<p>{t("install_new_calendar_app")}</p>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: true }]);
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
@@ -63,11 +63,45 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
return (
|
||||
<Select
|
||||
name="additionalCalendar"
|
||||
placeholder={t("install_another")}
|
||||
placeholder={
|
||||
<div className="flex justify-start text-black">
|
||||
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
|
||||
<p>{t("add")}</p>
|
||||
</div>
|
||||
}
|
||||
className="min-w-44"
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
isLoading={isLoading}
|
||||
components={{ Option: ImageOption }}
|
||||
styles={{
|
||||
menu: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
"@media only screen and (max-width: 640px)": {
|
||||
left: "0",
|
||||
},
|
||||
width: "max-content",
|
||||
right: "0",
|
||||
}),
|
||||
control: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
padding: "0",
|
||||
border: "0",
|
||||
borderRadius: "inherit",
|
||||
}),
|
||||
dropdownIndicator: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
display: "none",
|
||||
}),
|
||||
valueContainer: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
padding: "0",
|
||||
}),
|
||||
placeholder: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
margin: "0",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
|
||||
export default function AllApps({ apps }: { apps: App[] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => (
|
||||
<AppCard key={app.name} app={app} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,10 +10,13 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App as AppType } from "@calcom/types/App";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Button } from "@calcom/ui/components";
|
||||
import { showToast, SkeletonText } from "@calcom/ui/v2";
|
||||
import { Button, SkeletonButton, Shell } from "@calcom/ui/v2";
|
||||
import { SkeletonButton, Shell } from "@calcom/ui/v2";
|
||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||
|
||||
import HeadSeo from "@components/seo/head-seo";
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
type,
|
||||
@@ -55,18 +58,21 @@ const Component = ({
|
||||
}).format(price);
|
||||
|
||||
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||
const appCredentials = trpc.useQuery(["viewer.appCredentialsByType", { appType: type }], {
|
||||
onSuccess(data) {
|
||||
setExistingCredentials(data);
|
||||
},
|
||||
});
|
||||
const appCredentials = trpc.viewer.appCredentialsByType.useQuery(
|
||||
{ appType: type },
|
||||
{
|
||||
onSuccess(data) {
|
||||
setExistingCredentials(data);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||
{hasImages && (
|
||||
<div className="flex-2 mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:rounded-md">
|
||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||
{images ? (
|
||||
images.map((img) => (
|
||||
<img
|
||||
@@ -83,7 +89,7 @@ const Component = ({
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
"sticky top-0 -mt-4 max-w-xl flex-1 pb-12 text-sm lg:pb-0",
|
||||
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||
hasImages && "lg:ml-8"
|
||||
)}>
|
||||
<div className="mb-8 flex pt-4">
|
||||
@@ -140,6 +146,7 @@ const Component = ({
|
||||
</div>
|
||||
) : existingCredentials.length > 0 ? (
|
||||
<DisconnectIntegration
|
||||
buttonProps={{ color: "secondary" }}
|
||||
label={t("disconnect")}
|
||||
credentialId={existingCredentials[0]}
|
||||
onSuccess={() => {
|
||||
@@ -276,6 +283,7 @@ const Component = ({
|
||||
|
||||
export default function App(props: {
|
||||
name: string;
|
||||
description: AppType["description"];
|
||||
type: AppType["type"];
|
||||
isGlobal?: AppType["isGlobal"];
|
||||
logo: string;
|
||||
@@ -300,7 +308,16 @@ export default function App(props: {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell large isPublic heading={t("app_store")} backPath="/apps">
|
||||
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
||||
<HeadSeo
|
||||
title={props.name}
|
||||
description={props.description}
|
||||
app={{ slug: props.logo, name: props.name, description: props.description }}
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
{props.licenseRequired ? (
|
||||
<LicenseRequired>
|
||||
<Component {...props} />
|
||||
@@ -1,44 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { App } from "@calcom/types/App";
|
||||
import Badge from "@calcom/ui/Badge";
|
||||
import Button from "@calcom/ui/Button";
|
||||
|
||||
interface AppCardProps {
|
||||
app: App;
|
||||
}
|
||||
|
||||
export default function AppCard(props: AppCardProps) {
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
return (
|
||||
<Link href={"/apps/" + props.app.slug}>
|
||||
<a
|
||||
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
||||
data-testid={`app-store-app-card-${props.app.slug}`}>
|
||||
<div className="flex">
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={props.app.logo} alt={props.app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
||||
}
|
||||
<Button
|
||||
color="secondary"
|
||||
className="ml-auto flex self-start"
|
||||
onClick={() => {
|
||||
console.log("The magic is supposed to happen here");
|
||||
}}>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-medium">{props.app.name}</h3>
|
||||
</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>
|
||||
</div> */}
|
||||
<p className="mt-2 truncate text-sm text-gray-500">{props.app.description}</p>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
+47
-39
@@ -3,24 +3,24 @@ import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
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 { Icon } from "@calcom/ui/Icon";
|
||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
||||
import { Button } from "@calcom/ui/components";
|
||||
import { Alert, EmptyScreen } from "@calcom/ui/v2";
|
||||
import { List } from "@calcom/ui/v2/core/List";
|
||||
import { ShellSubHeading } from "@calcom/ui/v2/core/Shell";
|
||||
import Switch from "@calcom/ui/v2/core/Switch";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
import AdditionalCalendarSelector from "@components/v2/apps/AdditionalCalendarSelector";
|
||||
import DestinationCalendarSelector from "@components/v2/apps/DestinationCalendarSelector";
|
||||
import IntegrationListItem from "@components/v2/apps/IntegrationListItem";
|
||||
|
||||
type Props = {
|
||||
onChanged: () => unknown | Promise<unknown>;
|
||||
@@ -77,7 +77,7 @@ function CalendarSwitch(props: {
|
||||
},
|
||||
{
|
||||
async onSettled() {
|
||||
await utils.invalidateQueries(["viewer.integrations"]);
|
||||
await utils.viewer.integrations.invalidate();
|
||||
},
|
||||
onError() {
|
||||
showToast(`Something went wrong when toggling "${props.title}""`, "error");
|
||||
@@ -107,7 +107,7 @@ function CalendarSwitch(props: {
|
||||
|
||||
function CalendarList(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.integrations", { variant: "calendar", onlyInstalled: false }]);
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
|
||||
|
||||
return (
|
||||
<QueryCell
|
||||
@@ -141,9 +141,12 @@ function CalendarList(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings"
|
||||
function ConnectedCalendarsList(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"], { suspense: true });
|
||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
||||
suspense: true,
|
||||
});
|
||||
const { fromOnboarding } = props;
|
||||
return (
|
||||
<QueryCell
|
||||
@@ -163,6 +166,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
title={item.integration.title}
|
||||
logo={item.integration.logo}
|
||||
description={item.primary?.externalId || "No external Id"}
|
||||
separate={true}
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<DisconnectIntegration
|
||||
@@ -173,23 +177,27 @@ function ConnectedCalendarsList(props: Props) {
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="px-4 pt-4 text-sm text-neutral-500">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-2 p-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
defaultSelected={cal.isSelected}
|
||||
destination={cal.externalId === props.destinationCalendarId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<div className="border-t border-gray-200">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="px-4 pt-4 text-sm text-neutral-500">
|
||||
{t("toggle_calendars_conflict")}
|
||||
</p>
|
||||
<ul className="space-y-2 p-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
defaultSelected={cal.isSelected}
|
||||
destination={cal.externalId === props.destinationCalendarId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</IntegrationListItem>
|
||||
) : (
|
||||
<Alert
|
||||
@@ -229,19 +237,19 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
const utils = trpc.useContext();
|
||||
const onChanged = () =>
|
||||
Promise.allSettled([
|
||||
utils.invalidateQueries(["viewer.integrations", { variant: "calendar", onlyInstalled: true }], {
|
||||
exact: true,
|
||||
}),
|
||||
utils.invalidateQueries(["viewer.connectedCalendars"]),
|
||||
utils.viewer.integrations.invalidate(
|
||||
{ variant: "calendar", onlyInstalled: true },
|
||||
{
|
||||
exact: true,
|
||||
}
|
||||
),
|
||||
utils.viewer.connectedCalendars.invalidate(),
|
||||
]);
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
const installedCalendars = trpc.useQuery([
|
||||
"viewer.integrations",
|
||||
{ variant: "calendar", onlyInstalled: true },
|
||||
]);
|
||||
const mutation = trpc.useMutation("viewer.setDestinationCalendar", {
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
const mutation = trpc.viewer.setDestinationCalendar.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.invalidateQueries(["viewer.connectedCalendars"]);
|
||||
utils.viewer.connectedCalendars.invalidate();
|
||||
},
|
||||
});
|
||||
return (
|
||||
@@ -258,11 +266,11 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
<ShellSubHeading
|
||||
title={t("calendar")}
|
||||
subtitle={t("installed_app_calendar_description")}
|
||||
className="mb-0 flex flex-wrap items-center gap-4 md:mb-3 md:gap-0"
|
||||
className="mb-0 flex flex-wrap items-center gap-4 sm:flex-nowrap md:mb-3 md:gap-0"
|
||||
actions={
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||
{!!data.connectedCalendars.length && (
|
||||
<div className=" flex items-center">
|
||||
<div className="flex items-center">
|
||||
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
|
||||
</div>
|
||||
)}
|
||||
@@ -282,7 +290,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
|
||||
<p className="text-sm font-normal">{t("set_calendar")}</p>
|
||||
</div>
|
||||
<div className="flex justify-end md:w-6/12">
|
||||
<div className="justify-end md:w-6/12">
|
||||
<DestinationCalendarSelector
|
||||
onChange={mutation.mutate}
|
||||
hidePlaceholder
|
||||
@@ -1,43 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
export default function AppStoreCategories({
|
||||
categories,
|
||||
}: {
|
||||
categories: {
|
||||
name: string;
|
||||
count: number;
|
||||
}[];
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="mb-16">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
|
||||
<div className="grid-col-1 grid w-full gap-3 overflow-scroll sm:grid-flow-col">
|
||||
{categories.map((category) => (
|
||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
||||
<a
|
||||
data-testid={`app-store-category-${category.name}`}
|
||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||
<div className="min-w-24 -ml-5 text-center sm:ml-0">
|
||||
<Image
|
||||
alt={category.name}
|
||||
width="352"
|
||||
height="252"
|
||||
layout="responsive"
|
||||
src={"/app-store/" + category.name + ".svg"}
|
||||
/>
|
||||
</div>
|
||||
<div className="self-center">
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">{category.count} apps</p>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -23,7 +23,7 @@ const DestinationCalendarSelector = ({
|
||||
destinationCalendar,
|
||||
}: Props): JSX.Element | null => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
||||
|
||||
// Extra styles to show prefixed text in react-select
|
||||
+17
-2
@@ -2,6 +2,8 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
@@ -16,7 +18,10 @@ function IntegrationListItem(props: {
|
||||
children?: ReactNode;
|
||||
logo: string;
|
||||
destination?: boolean;
|
||||
separate?: boolean;
|
||||
invalidCredential?: boolean;
|
||||
}): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { hl } = router.query;
|
||||
const [highlight, setHighlight] = useState(hl === props.slug);
|
||||
@@ -32,7 +37,8 @@ function IntegrationListItem(props: {
|
||||
<ListItem
|
||||
expanded={!!props.children}
|
||||
className={classNames(
|
||||
"my-0 flex-col border transition-colors duration-500 first:rounded-t-md last:rounded-b-md",
|
||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
||||
"my-0 flex-col border transition-colors duration-500 ",
|
||||
highlight ? "bg-yellow-100" : ""
|
||||
)}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||
@@ -42,10 +48,19 @@ function IntegrationListItem(props: {
|
||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
{/* Alert error that key stopped working. */}
|
||||
{props.invalidCredential && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{props.actions}</div>
|
||||
</div>
|
||||
{props.children && <div className="w-full border-t border-gray-200">{props.children}</div>}
|
||||
{props.children && <div className="w-full">{props.children}</div>}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import useApp from "@calcom/lib/hooks/useApp";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { showToast } from "@calcom/ui/v2";
|
||||
|
||||
/**
|
||||
* Use this component to allow installing an app from anywhere on the app.
|
||||
* Use of this component requires you to remove custom InstallAppButtonComponent so that it can manage the redirection itself
|
||||
*/
|
||||
export default function OmniInstallAppButton({ appId, className }: { appId: string; className: string }) {
|
||||
const { t } = useLocale();
|
||||
const { data: app } = useApp(appId);
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const mutation = useAddAppMutation(null, {
|
||||
onSuccess: () => {
|
||||
//TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated.
|
||||
utils.viewer.appById.invalidate({ appId });
|
||||
utils.viewer.apps.invalidate({ extendsFeature: "EventType" });
|
||||
showToast(t("app_successfully_installed"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InstallAppButton
|
||||
type={app.type}
|
||||
isProOnly={app.isProOnly}
|
||||
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
|
||||
render={({ useDefaultComponent, ...props }) => {
|
||||
if (useDefaultComponent) {
|
||||
props = {
|
||||
onClick: () => {
|
||||
mutation.mutate({ type: app.type, variant: app.variant, slug: app.slug, isOmniInstall: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
color="secondary"
|
||||
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
|
||||
StartIcon={Icon.FiPlus}
|
||||
{...props}>
|
||||
{t("install")}
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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 { useEffect, useRef } from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
const Slider = <T extends string | unknown>({
|
||||
title = "",
|
||||
className = "",
|
||||
items,
|
||||
itemKey = (item) => `${item}`,
|
||||
renderItem,
|
||||
options = {},
|
||||
}: {
|
||||
title?: string;
|
||||
className?: string;
|
||||
items: T[];
|
||||
itemKey?: (item: T) => string;
|
||||
renderItem?: (item: T) => JSX.Element;
|
||||
options?: Options;
|
||||
}) => {
|
||||
const glide = useRef(null);
|
||||
const slider = useRef<Glide.Properties | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (glide.current) {
|
||||
slider.current = new Glide(glide.current, {
|
||||
type: "carousel",
|
||||
...options,
|
||||
}).mount();
|
||||
}
|
||||
|
||||
return () => slider.current?.destroy();
|
||||
}, [options]);
|
||||
|
||||
return (
|
||||
<div className={`mb-2 ${className}`}>
|
||||
<style jsx global>
|
||||
{`
|
||||
.glide__slide {
|
||||
height: auto !important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="glide" ref={glide}>
|
||||
<div className="flex cursor-default">
|
||||
{title && (
|
||||
<div>
|
||||
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">{title}</h2>
|
||||
</div>
|
||||
)}
|
||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
||||
<button data-glide-dir="<" className="mr-4">
|
||||
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
<button data-glide-dir=">">
|
||||
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="glide__track" data-glide-el="track">
|
||||
<ul className="glide__slides">
|
||||
{items.map((item) => {
|
||||
if (typeof renderItem !== "function") return null;
|
||||
return (
|
||||
<li key={itemKey(item)} className="glide__slide h-auto pl-0">
|
||||
{renderItem(item)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { App } from "@calcom/types/App";
|
||||
|
||||
import AppCard from "./AppCard";
|
||||
import Slider from "./Slider";
|
||||
|
||||
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Slider<T>
|
||||
className="mb-16"
|
||||
title={t("trending_apps")}
|
||||
items={items.filter((app) => !!app.trending)}
|
||||
itemKey={(app) => app.name}
|
||||
options={{
|
||||
perView: 3,
|
||||
breakpoints: {
|
||||
768 /* and below */: {
|
||||
perView: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderItem={(app) => <AppCard app={app} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */
|
||||
export default TrendingAppsSlider;
|
||||
@@ -1,62 +0,0 @@
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
samlTenantID: string;
|
||||
samlProductID: string;
|
||||
hostedCal: boolean;
|
||||
setErrorMessage: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
export default function SAMLLogin(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const methods = useFormContext();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
|
||||
onSuccess: async (data) => {
|
||||
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
||||
},
|
||||
onError: (err) => {
|
||||
props.setErrorMessage(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="saml"
|
||||
className="flex w-full justify-center"
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// track Google logins. Without personal data/payload
|
||||
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
||||
|
||||
if (!props.hostedCal) {
|
||||
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
|
||||
} else {
|
||||
if (props.email.length === 0) {
|
||||
props.setErrorMessage(t("saml_email_required"));
|
||||
return;
|
||||
}
|
||||
// hosted solution, fetch tenant and product from the backend
|
||||
mutation.mutate({
|
||||
email: methods.getValues("email"),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{t("signin_with_saml")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
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";
|
||||
|
||||
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
const form = useForm<{
|
||||
name: string;
|
||||
}>();
|
||||
const { register } = form;
|
||||
|
||||
const createMutation = trpc.useMutation("viewer.availability.schedule.create", {
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push("/availability/" + schedule.id);
|
||||
showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not able to create this event`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog name={name} clearQueryParamsOnClose={["copy-schedule-id"]}>
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid={name} StartIcon={Icon.FiPlus}>
|
||||
{t("new_schedule_btn")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{t("add_new_schedule")}
|
||||
</h3>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-2">
|
||||
<label htmlFor="label" className="block text-sm font-medium text-gray-700">
|
||||
{t("name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
placeholder={t("default_schedule_name")}
|
||||
{...register("name")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="secondary">{t("cancel")}</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
import classNames from "classnames";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
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, { 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";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import { TimeRange } from "@lib/types/schedule";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
/** Begin Time Increments For Select */
|
||||
const increment = 15;
|
||||
|
||||
type Option = {
|
||||
readonly label: string;
|
||||
readonly value: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of times on a 15 minute interval from
|
||||
* 00:00:00 (Start of day) to
|
||||
* 23:45:00 (End of day with enough time for 15 min booking)
|
||||
*/
|
||||
const useOptions = () => {
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const { timeFormat } = query.data || { timeFormat: null };
|
||||
|
||||
const [filteredOptions, setFilteredOptions] = useState<Option[]>([]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
const end = dayjs().utc().endOf("day");
|
||||
let t: Dayjs = dayjs().utc().startOf("day");
|
||||
|
||||
const options: Option[] = [];
|
||||
while (t.isBefore(end)) {
|
||||
options.push({
|
||||
value: t.toDate().valueOf(),
|
||||
label: dayjs(t)
|
||||
.utc()
|
||||
.format(timeFormat === 12 ? "h:mma" : "HH:mm"),
|
||||
});
|
||||
t = t.add(increment, "minutes");
|
||||
}
|
||||
return options;
|
||||
}, [timeFormat]);
|
||||
|
||||
const filter = useCallback(
|
||||
({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => {
|
||||
if (current) {
|
||||
const currentOption = options.find((option) => option.value === dayjs(current).toDate().valueOf());
|
||||
if (currentOption) setFilteredOptions([currentOption]);
|
||||
} else
|
||||
setFilteredOptions(
|
||||
options.filter((option) => {
|
||||
const time = dayjs(option.value);
|
||||
return (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset));
|
||||
})
|
||||
);
|
||||
},
|
||||
[options]
|
||||
);
|
||||
|
||||
return { options: filteredOptions, filter };
|
||||
};
|
||||
|
||||
type TimeRangeFieldProps = {
|
||||
name: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const LazySelect = ({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
...props
|
||||
}: Omit<Props<Option, false, GroupBase<Option>>, "value"> & {
|
||||
value: ConfigType;
|
||||
min?: ConfigType;
|
||||
max?: ConfigType;
|
||||
}) => {
|
||||
// Lazy-loaded options, otherwise adding a field has a noticable redraw delay.
|
||||
const { options, filter } = useOptions();
|
||||
|
||||
useEffect(() => {
|
||||
filter({ current: value });
|
||||
}, [filter, value]);
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
onMenuOpen={() => {
|
||||
if (min) filter({ offset: min });
|
||||
if (max) filter({ limit: max });
|
||||
}}
|
||||
value={options.find((option) => option.value === dayjs(value).toDate().valueOf())}
|
||||
onMenuClose={() => filter({ current: value })}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TimeRangeField = ({ name, className }: TimeRangeFieldProps) => {
|
||||
const { watch } = useFormContext();
|
||||
const minEnd = watch(`${name}.start`);
|
||||
const maxStart = watch(`${name}.end`);
|
||||
return (
|
||||
<div className={classNames("flex flex-grow items-center space-x-3", className)}>
|
||||
<Controller
|
||||
name={`${name}.start`}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<LazySelect
|
||||
className="w-[120px]"
|
||||
value={value}
|
||||
max={maxStart}
|
||||
onChange={(option) => {
|
||||
onChange(new Date(option?.value as number));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span>-</span>
|
||||
<Controller
|
||||
name={`${name}.end`}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<LazySelect
|
||||
className="flex-grow sm:w-[120px]"
|
||||
value={value}
|
||||
min={minEnd}
|
||||
onChange={(option) => {
|
||||
onChange(new Date(option?.value as number));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type ScheduleBlockProps = {
|
||||
day: number;
|
||||
weekday: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (selected: number[]) => void }) => {
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const { i18n, t } = useLocale();
|
||||
return (
|
||||
<div className="m-4 space-y-2 py-4">
|
||||
<p className="h6 text-xs font-medium uppercase text-neutral-400">Copy times to</p>
|
||||
<ol className="space-y-2">
|
||||
{weekdayNames(i18n.language).map((weekday, num) => (
|
||||
<li key={weekday}>
|
||||
<label className="flex w-full items-center justify-between">
|
||||
<span>{weekday}</span>
|
||||
<input
|
||||
value={num}
|
||||
defaultChecked={disabled.includes(num)}
|
||||
disabled={disabled.includes(num)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked && !selected.includes(num)) {
|
||||
setSelected(selected.concat([num]));
|
||||
} else if (!e.target.checked && selected.includes(num)) {
|
||||
setSelected(selected.slice(selected.indexOf(num), 1));
|
||||
}
|
||||
}}
|
||||
type="checkbox"
|
||||
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
|
||||
/>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<div className="pt-2">
|
||||
<Button className="w-full justify-center" color="primary" onClick={() => onApply(selected)}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
export const DayRanges = ({
|
||||
name,
|
||||
defaultValue = [defaultDayRange],
|
||||
}: {
|
||||
name: string;
|
||||
defaultValue?: TimeRange[];
|
||||
}) => {
|
||||
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,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultValue.length && !fields.length) {
|
||||
replace(defaultValue);
|
||||
}
|
||||
}, [replace, defaultValue, fields.length]);
|
||||
|
||||
const handleAppend = () => {
|
||||
// FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499
|
||||
const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end);
|
||||
const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour");
|
||||
|
||||
if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) {
|
||||
return append({
|
||||
start: nextRangeStart.toDate(),
|
||||
end: nextRangeEnd.toDate(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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 space-x-1 sm:flex-grow-0">
|
||||
<TimeRangeField name={`${name}.${index}`} />
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="minimal"
|
||||
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">
|
||||
<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>
|
||||
<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)]}
|
||||
onApply={(selected) =>
|
||||
selected.forEach((day) => {
|
||||
// TODO: Figure out why this is different?
|
||||
// console.log(watcher, fields);
|
||||
setValue(name.substring(0, name.lastIndexOf(".") + 1) + day, watcher);
|
||||
})
|
||||
}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const form = useFormContext();
|
||||
const watchAvailable = form.watch(`${name}.${day}`, []);
|
||||
|
||||
return (
|
||||
<fieldset className="relative flex flex-col justify-between space-y-2 py-5 sm:flex-row sm:space-y-0">
|
||||
<label
|
||||
className={classNames(
|
||||
"flex space-x-2 rtl:space-x-reverse",
|
||||
!watchAvailable.length ? "w-full" : "w-1/3"
|
||||
)}>
|
||||
<div className={classNames(!watchAvailable.length ? "w-1/3" : "w-full")}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={watchAvailable.length}
|
||||
onChange={(e) => {
|
||||
form.setValue(`${name}.${day}`, e.target.checked ? [defaultDayRange] : []);
|
||||
}}
|
||||
className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500"
|
||||
/>
|
||||
<span className="ml-2 inline-block text-sm capitalize">{weekday}</span>
|
||||
</div>
|
||||
{!watchAvailable.length && (
|
||||
<div className="flex-grow text-right text-sm text-gray-500 sm:flex-shrink">
|
||||
{t("no_availability")}
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
{!!watchAvailable.length && (
|
||||
<div className="flex-grow">
|
||||
<DayRanges name={`${name}.${day}`} defaultValue={[]} />
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* use Component in "/packages/features/schedules/components/Schedule";
|
||||
**/
|
||||
const Schedule = ({ name }: { name: string }) => {
|
||||
const { i18n } = useLocale();
|
||||
return (
|
||||
<fieldset className="divide-y divide-gray-200">
|
||||
{weekdayNames(i18n.language).map((weekday, num) => (
|
||||
<ScheduleBlock key={num} name={name} weekday={weekday} day={num} />
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
|
||||
export default Schedule;
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||
*/
|
||||
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 { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
/**
|
||||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/packages/features/schedules/components/ScheduleListItem.tsx` instead
|
||||
*/
|
||||
export function ScheduleListItem({
|
||||
schedule,
|
||||
deleteFunction,
|
||||
isDeleting = false,
|
||||
}: {
|
||||
schedule: inferQueryOutput<"viewer.availability.list">["schedules"][number];
|
||||
deleteFunction: ({ scheduleId }: { scheduleId: number }) => void;
|
||||
isDeleting: boolean;
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
|
||||
return (
|
||||
<li key={schedule.id}>
|
||||
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
||||
<div className="group flex w-full items-center justify-between hover:bg-neutral-50 sm:px-6">
|
||||
<Link href={"/availability/" + schedule.id}>
|
||||
<a className="flex-grow truncate text-sm" title={schedule.name}>
|
||||
<div>
|
||||
<span className="truncate font-medium text-neutral-900">{schedule.name}</span>
|
||||
{schedule.isDefault && (
|
||||
<span className="ml-2 inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
|
||||
{t("default")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-neutral-500">
|
||||
{schedule.availability.map((availability: Availability) => (
|
||||
<Fragment key={availability.id}>
|
||||
{availabilityAsString(availability, { locale: i18n.language })}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
|
||||
<Icon.FiMoreHorizontal className="h-5 w-5 group-hover:text-gray-800" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
deleteFunction({
|
||||
scheduleId: schedule.id,
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
color="warn"
|
||||
className="w-full font-normal"
|
||||
StartIcon={isDeleting ? undefined : Icon.FiTrash}
|
||||
loading={isDeleting}>
|
||||
{isDeleting ? t("deleting") : t("delete_schedule")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
/** @deprecated Use `apps/web/components/v2/availability/SkeletonLoader.tsx` */
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:mx-0 sm:overflow-hidden">
|
||||
@@ -20,26 +21,30 @@ 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 width="32" height="2" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-2 w-32" />
|
||||
</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 className="h-6 w-12" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const AvailabilitySelectSkeletonLoader = () => {
|
||||
export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<li className="group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3">
|
||||
<li
|
||||
className={classNames(
|
||||
"group flex w-full items-center justify-between rounded-sm border border-gray-200 px-[10px] py-3",
|
||||
className
|
||||
)}>
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText width="4" height="4" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
<SkeletonText className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
||||
|
||||
import { Props } from "./pages/AvailabilityPage";
|
||||
|
||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||
return locations.length ? (
|
||||
<div>
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||
{locations.map((location) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
<span key={location.type}>{locationKeyToString(location)} </span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||
{locations.map((location) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
<Tooltip content={locationKeyToString(location)}>
|
||||
<p className="truncate">{locationKeyToString(location)}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
||||
@@ -4,15 +4,18 @@ import { FC, useEffect, useState } from "react";
|
||||
|
||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
|
||||
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { timeZone } from "@lib/clock";
|
||||
|
||||
type AvailableTimesProps = {
|
||||
timeFormat: string;
|
||||
timeFormat: TimeFormat;
|
||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||
eventTypeId: number;
|
||||
recurringCount: number | undefined;
|
||||
eventTypeSlug: string;
|
||||
@@ -31,6 +34,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
eventTypeSlug,
|
||||
recurringCount,
|
||||
timeFormat,
|
||||
onTimeFormatChange,
|
||||
seatsPerTimeSlot,
|
||||
ethSignature,
|
||||
}) => {
|
||||
@@ -48,16 +52,27 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
|
||||
return (
|
||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||
<div className="mb-4 text-left text-base">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 mb-8 w-1/2 break-words font-semibold text-gray-900">
|
||||
{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 className="mb-6 flex items-center text-left text-base">
|
||||
<div className="mr-4">
|
||||
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<ToggleGroup
|
||||
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||
options={[
|
||||
{ value: "12", label: t("12_hour_short") },
|
||||
{ value: "24", label: t("24_hour_short") },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||
{slots.length > 0 &&
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
@@ -65,10 +80,10 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
query: Record<string, string | number | string[] | undefined>;
|
||||
};
|
||||
const bookingUrl: BookingURL = {
|
||||
pathname: "book",
|
||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||
query: {
|
||||
...router.query,
|
||||
date: dayjs(slot.time).format(),
|
||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||
type: eventTypeId,
|
||||
slug: eventTypeSlug,
|
||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||
@@ -137,13 +152,13 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
{isLoading && !slots.length && (
|
||||
<>
|
||||
<SkeletonContainer className="mb-2">
|
||||
<SkeletonText width="full" height="20" />
|
||||
<SkeletonText className="h-5 w-full" />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className="mb-2">
|
||||
<SkeletonText width="full" height="20" />
|
||||
<SkeletonText className="h-5 w-full" />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className="mb-2">
|
||||
<SkeletonText width="full" height="20" />
|
||||
<SkeletonText className="h-5 w-full" />
|
||||
</SkeletonContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
|
||||
import { UserAvatars } from "@components/booking/UserAvatars";
|
||||
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
|
||||
|
||||
import type { AvailabilityPageProps } from "../../pages/[user]/[type]";
|
||||
import type { BookPageProps } from "../../pages/[user]/book";
|
||||
import type { DynamicAvailabilityPageProps } from "../../pages/d/[link]/[slug]";
|
||||
import type { HashLinkPageProps } from "../../pages/d/[link]/book";
|
||||
import type { AvailabilityTeamPageProps } from "../../pages/team/[slug]/[type]";
|
||||
import type { TeamBookingPageProps } from "../../pages/team/[slug]/book";
|
||||
import { AvailableEventLocations } from "./AvailableEventLocations";
|
||||
|
||||
interface Props {
|
||||
profile:
|
||||
| AvailabilityPageProps["profile"]
|
||||
| HashLinkPageProps["profile"]
|
||||
| TeamBookingPageProps["profile"]
|
||||
| BookPageProps["profile"]
|
||||
| AvailabilityTeamPageProps["profile"]
|
||||
| DynamicAvailabilityPageProps["profile"];
|
||||
eventType:
|
||||
| AvailabilityPageProps["eventType"]
|
||||
| HashLinkPageProps["eventType"]
|
||||
| TeamBookingPageProps["eventType"]
|
||||
| BookPageProps["eventType"]
|
||||
| AvailabilityTeamPageProps["eventType"]
|
||||
| DynamicAvailabilityPageProps["eventType"];
|
||||
isBookingPage?: boolean;
|
||||
children: ReactNode;
|
||||
isMobile?: boolean;
|
||||
rescheduleUid?: string;
|
||||
}
|
||||
|
||||
const BookingDescription: FC<Props> = (props) => {
|
||||
const { profile, eventType, isBookingPage = false, children } = props;
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<UserAvatars
|
||||
profile={profile}
|
||||
users={eventType.users}
|
||||
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
|
||||
size={10}
|
||||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-2 break-words text-sm font-medium text-gray-600 dark:text-gray-300">
|
||||
{profile.name}
|
||||
</h2>
|
||||
<h1 className="font-cal dark:text-darkgray-900 mb-6 break-words text-2xl font-semibold text-gray-900">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="dark:text-darkgray-600 flex flex-col space-y-3 text-sm font-medium text-gray-600">
|
||||
{eventType?.description && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex",
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiInfo
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] inline-block h-4 w-4",
|
||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{eventType?.requiresConfirmation && (
|
||||
<div
|
||||
className={classNames(
|
||||
"flex items-center",
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 " />
|
||||
</div>
|
||||
{t("requires_confirmation")}
|
||||
</div>
|
||||
)}
|
||||
<AvailableEventLocations
|
||||
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
||||
/>
|
||||
<p
|
||||
className={classNames(
|
||||
"text-sm font-medium",
|
||||
isBookingPage && "dark:text-darkgray-600 text-gray-600"
|
||||
)}>
|
||||
<Icon.FiClock className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingDescription;
|
||||
@@ -1,55 +1,60 @@
|
||||
import { BookingStatus } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
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 { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextArea } from "@calcom/ui/form/fields";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
|
||||
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
import { extractRecurringDates } from "@lib/parseDate";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||
|
||||
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
||||
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
type BookingItemProps = BookingItem & {
|
||||
listingStatus: BookingListingStatus;
|
||||
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
|
||||
recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
|
||||
};
|
||||
|
||||
function BookingListItem(booking: BookingItemProps) {
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const user = query.data;
|
||||
const { t, i18n } = useLocale();
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
|
||||
onSuccess: () => {
|
||||
setRejectionDialogIsOpen(false);
|
||||
showToast(t("booking_confirmation_success"), "success");
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.status === BookingStatus.REJECTED) {
|
||||
setRejectionDialogIsOpen(false);
|
||||
showToast(t("booking_rejection_success"), "success");
|
||||
} else {
|
||||
showToast(t("booking_confirmation_success"), "success");
|
||||
}
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("booking_confirmation_failed"), "error");
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -107,7 +112,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
||||
/* 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}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`,
|
||||
href: `/success?uid=${booking.uid}&cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}`,
|
||||
icon: Icon.FiX,
|
||||
},
|
||||
{
|
||||
@@ -157,11 +164,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||
const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", {
|
||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("location_updated"), "success");
|
||||
setIsOpenLocationDialog(false);
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -174,37 +181,24 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||
};
|
||||
|
||||
// Calculate the booking date(s) and setup recurring event data to show
|
||||
let recurringStrings: string[] = [];
|
||||
let recurringDates: Date[] = [];
|
||||
// Getting accepted recurring dates to show
|
||||
const recurringDates = booking.recurringInfo?.bookings[BookingStatus.ACCEPTED]
|
||||
.concat(booking.recurringInfo?.bookings[BookingStatus.CANCELLED])
|
||||
.concat(booking.recurringInfo?.bookings[BookingStatus.PENDING])
|
||||
.sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
|
||||
|
||||
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||
[recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n);
|
||||
}
|
||||
|
||||
const location = booking.location || "";
|
||||
|
||||
const onClick = () => {
|
||||
const onClickTableData = () => {
|
||||
router.push({
|
||||
pathname: "/success",
|
||||
query: {
|
||||
date: booking.startTime,
|
||||
// TODO: Booking when fetched should have id 0 already(for Dynamic Events).
|
||||
type: booking.eventType.id || 0,
|
||||
eventSlug: booking.eventType.slug,
|
||||
user: user?.username || "",
|
||||
name: booking.attendees[0] ? booking.attendees[0].name : undefined,
|
||||
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||
location: location,
|
||||
eventName: booking.eventType.eventName || "",
|
||||
bookingId: booking.id,
|
||||
recur: booking.recurringEventId,
|
||||
reschedule: isConfirmed,
|
||||
uid: booking.uid,
|
||||
allRemainingBookings: isTabRecurring,
|
||||
listingStatus: booking.listingStatus,
|
||||
status: booking.status,
|
||||
email: booking.attendees[0] ? booking.attendees[0].email : undefined,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -253,20 +247,27 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</Dialog>
|
||||
|
||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[10rem]" onClick={onClick}>
|
||||
<td
|
||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||
onClick={onClickTableData}>
|
||||
<div className="cursor-pointer py-4">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||
<MeetingTimeInTimezones
|
||||
timeFormat={user?.timeFormat}
|
||||
userTimezone={user?.timeZone}
|
||||
startTime={booking.startTime}
|
||||
endTime={booking.endTime}
|
||||
attendees={booking.attendees}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isPending && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("unconfirmed")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{booking.eventType?.team && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="gray">
|
||||
{booking.eventType.team.name}
|
||||
@@ -277,24 +278,28 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
<RecurringBookingsTooltip
|
||||
booking={booking}
|
||||
recurringStrings={recurringStrings}
|
||||
recurringDates={recurringDates}
|
||||
/>
|
||||
</div>
|
||||
{recurringDates !== undefined && (
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
|
||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
||||
{/* Time and Badges for mobile */}
|
||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||
<div className="flex w-full items-center justify-between sm:hidden">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="pr-2 text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")} -{" "}
|
||||
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
||||
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||
<MeetingTimeInTimezones
|
||||
timeFormat={user?.timeFormat}
|
||||
userTimezone={user?.timeZone}
|
||||
startTime={booking.startTime}
|
||||
endTime={booking.endTime}
|
||||
attendees={booking.attendees}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -313,13 +318,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="text-sm text-gray-400 sm:hidden">
|
||||
<RecurringBookingsTooltip
|
||||
booking={booking}
|
||||
recurringStrings={recurringStrings}
|
||||
recurringDates={recurringDates}
|
||||
/>
|
||||
</div>
|
||||
{recurringDates !== undefined && (
|
||||
<div className="text-sm text-gray-400 sm:hidden">
|
||||
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="cursor-pointer py-4">
|
||||
@@ -338,7 +341,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</div>
|
||||
{booking.description && (
|
||||
<div
|
||||
className="max-w-52 md:max-w-96 truncate text-sm text-gray-600"
|
||||
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
||||
title={booking.description}>
|
||||
"{booking.description}"
|
||||
</div>
|
||||
@@ -379,20 +382,26 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
interface RecurringBookingsTooltipProps {
|
||||
booking: BookingItemProps;
|
||||
recurringStrings: string[];
|
||||
recurringDates: Date[];
|
||||
}
|
||||
|
||||
const RecurringBookingsTooltip = ({
|
||||
booking,
|
||||
recurringStrings,
|
||||
recurringDates,
|
||||
}: RecurringBookingsTooltipProps) => {
|
||||
const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => {
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const user = query.data;
|
||||
const { t } = useLocale();
|
||||
const now = new Date();
|
||||
const recurringCount = recurringDates.filter((date) => {
|
||||
return (
|
||||
date >= now &&
|
||||
!booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||
.map((date) => date.toDateString())
|
||||
.includes(date.toDateString())
|
||||
);
|
||||
}).length;
|
||||
|
||||
return (
|
||||
(booking.recurringBookings &&
|
||||
(booking.recurringInfo &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
(booking.listingStatus === "recurring" ||
|
||||
booking.listingStatus === "unconfirmed" ||
|
||||
@@ -400,11 +409,20 @@ const RecurringBookingsTooltip = ({
|
||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||
<div className="flex">
|
||||
<Tooltip
|
||||
content={recurringStrings.map((aDate, key) => (
|
||||
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
|
||||
{aDate}
|
||||
</p>
|
||||
))}>
|
||||
content={recurringDates.map((aDate, key) => {
|
||||
const pastOrCancelled =
|
||||
aDate < now ||
|
||||
booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
|
||||
.map((date) => date.toDateString())
|
||||
.includes(aDate.toDateString());
|
||||
return (
|
||||
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
||||
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
||||
{" - "}
|
||||
{dayjs(aDate).format("D MMMM YYYY")}
|
||||
</p>
|
||||
);
|
||||
})}>
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCcw
|
||||
strokeWidth="3"
|
||||
@@ -413,14 +431,12 @@ const RecurringBookingsTooltip = ({
|
||||
<p className="mt-1 pl-5 text-xs">
|
||||
{booking.status === BookingStatus.ACCEPTED
|
||||
? `${t("event_remaining", {
|
||||
count: recurringDates.length,
|
||||
count: recurringCount,
|
||||
})}`
|
||||
: getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: booking.eventType.recurringEvent,
|
||||
recurringCount: recurringDates.filter((date) => {
|
||||
return date >= now;
|
||||
}).length,
|
||||
recurringCount: booking.recurringInfo.count,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -445,8 +461,9 @@ const FirstAttendee = ({
|
||||
user: UserProps;
|
||||
currentEmail: string | null | undefined;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return user.email === currentEmail ? (
|
||||
<div className="inline-block">You</div>
|
||||
<div className="inline-block">{t("you")}</div>
|
||||
) : (
|
||||
<a
|
||||
key={user.email}
|
||||
@@ -458,19 +475,19 @@ const FirstAttendee = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Attendee: React.FC<{ email: string; children: React.ReactNode }> = ({ email, children }) => {
|
||||
type AttendeeProps = {
|
||||
name?: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
const Attendee = ({ email, name }: AttendeeProps) => {
|
||||
return (
|
||||
<a className=" hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
<a className="hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
|
||||
{name || email}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface AttendeeProps {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const DisplayAttendees = ({
|
||||
attendees,
|
||||
user,
|
||||
@@ -478,42 +495,33 @@ const DisplayAttendees = ({
|
||||
}: {
|
||||
attendees: AttendeeProps[];
|
||||
user: UserProps | null;
|
||||
currentEmail: string | null | undefined;
|
||||
currentEmail?: string | null;
|
||||
}) => {
|
||||
if (attendees.length === 1) {
|
||||
return (
|
||||
<div className="text-sm text-gray-900">
|
||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||
<span> and </span>
|
||||
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||
</div>
|
||||
);
|
||||
} else if (attendees.length === 2) {
|
||||
return (
|
||||
<div className="text-sm text-gray-900">
|
||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||
<span>, </span>
|
||||
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||
<div className="inline-block text-sm text-gray-900"> and </div>
|
||||
<Attendee email={attendees[1].email}>{attendees[1].name}</Attendee>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="text-sm text-gray-900">
|
||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||
<span>, </span>
|
||||
<Attendee email={attendees[0].email}>{attendees[0].name}</Attendee>
|
||||
<span> & </span>
|
||||
<Tooltip
|
||||
content={attendees.slice(1).map((attendee, key) => (
|
||||
<p key={key}>{attendee.name}</p>
|
||||
))}>
|
||||
<div className="inline-block">{attendees.length - 1} more</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="text-sm text-gray-900">
|
||||
{user && <FirstAttendee user={user} currentEmail={currentEmail} />}
|
||||
{attendees.length > 1 ? <span>, </span> : <span> {t("and")} </span>}
|
||||
<Attendee {...attendees[0]} />
|
||||
{attendees.length > 1 && (
|
||||
<>
|
||||
<div className="inline-block text-sm text-gray-900"> {t("and")} </div>
|
||||
{attendees.length > 2 ? (
|
||||
<Tooltip
|
||||
content={attendees.slice(1).map((attendee) => (
|
||||
<p key={attendee.email}>
|
||||
<Attendee {...attendee} />
|
||||
</p>
|
||||
))}>
|
||||
<div className="inline-block">{t("plus_more", { count: attendees.length - 1 })}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Attendee {...attendees[1]} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
||||
|
||||
@@ -5,13 +5,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { Icon } from "@calcom/ui/Icon";
|
||||
import { Button, TextArea } from "@calcom/ui/components";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
title?: string;
|
||||
uid?: string;
|
||||
id?: number;
|
||||
};
|
||||
profile: {
|
||||
name: string | null;
|
||||
@@ -21,13 +22,14 @@ type Props = {
|
||||
team?: string | null;
|
||||
setIsCancellationMode: (value: boolean) => void;
|
||||
theme: string | null;
|
||||
allRemainingBookings: boolean;
|
||||
};
|
||||
|
||||
export default function CancelBooking(props: Props) {
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { booking, profile, team } = props;
|
||||
const { booking, profile, team, allRemainingBookings } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
@@ -50,37 +52,31 @@ export default function CancelBooking(props: Props) {
|
||||
{!error && (
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<label className="text-bookingdark font-medium dark:text-white">{t("cancellation_reason")}</label>
|
||||
<textarea
|
||||
<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 "
|
||||
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
|
||||
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
|
||||
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)}>
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse ">
|
||||
<div className="ml-auto flex w-full space-x-4 ">
|
||||
<Button
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
onClick={() => props.setIsCancellationMode(false)}>
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex grow justify-center"
|
||||
className="flex justify-center"
|
||||
data-testid="cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
const payload = {
|
||||
uid: booking?.uid,
|
||||
reason: cancellationReason,
|
||||
id: booking?.id,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
};
|
||||
|
||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
@@ -97,7 +93,7 @@ export default function CancelBooking(props: Props) {
|
||||
await router.push(
|
||||
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
|
||||
profile.slug
|
||||
}&team=${team ? 1 : 0}&recurring=${!!props.recurringEvent}`
|
||||
}&team=${team ? 1 : 0}&allRemainingBookings=${allRemainingBookings}`
|
||||
);
|
||||
} else {
|
||||
setLoading(false);
|
||||
@@ -109,7 +105,7 @@ export default function CancelBooking(props: Props) {
|
||||
}
|
||||
}}
|
||||
loading={loading}>
|
||||
{t("cancel_event")}
|
||||
{props.allRemainingBookings ? t("cancel_all_remaining") : t("cancel_event")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui/v2";
|
||||
|
||||
/** @deprecated Use `apps/web/components/v2/bookings/SkeletonLoader.tsx` */
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="animate-pulse divide-y divide-neutral-200 rounded-md border border-gray-200 bg-white sm:overflow-hidden">
|
||||
@@ -21,15 +20,15 @@ function SkeletonItem() {
|
||||
<div className="flex-grow truncate text-sm">
|
||||
<div className="flex">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<SkeletonText width="16" height="5" />
|
||||
<SkeletonText width="32" height="4" />
|
||||
<SkeletonText className="h-5 w-16" />
|
||||
<SkeletonText className="h-4 w-32" />
|
||||
</div>
|
||||
</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="16" height="6" />
|
||||
<SkeletonText width="32" height="6" />
|
||||
<SkeletonText className="h-6 w-16" />
|
||||
<SkeletonText className="h-6 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Switch } from "@calcom/ui/v2";
|
||||
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect";
|
||||
|
||||
import { is24h, timeZone } from "../../lib/clock";
|
||||
import { timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
onSelectTimeZone: (selectedTimeZone: string) => void;
|
||||
onToggle24hClock: (is24hClock: boolean) => void;
|
||||
timeFormat: string;
|
||||
hideTimeFormatToggle?: boolean;
|
||||
};
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat, hideTimeFormatToggle }) => {
|
||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const [is24hClock, setIs24hClock] = useState(timeFormat === "HH:mm" && true);
|
||||
const { t } = useLocale();
|
||||
|
||||
useEffect(() => {
|
||||
setIs24hClock(is24h());
|
||||
setSelectedTimeZone(timeZone());
|
||||
}, []);
|
||||
|
||||
@@ -28,28 +22,11 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
|
||||
onSelectTimeZone(timeZone(selectedTimeZone));
|
||||
}
|
||||
}, [selectedTimeZone, onSelectTimeZone]);
|
||||
const handle24hClockToggle = (is24hClock: boolean) => {
|
||||
setIs24hClock(is24hClock);
|
||||
onToggle24hClock(is24h(is24hClock));
|
||||
};
|
||||
|
||||
return selectedTimeZone !== "" ? (
|
||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
||||
<div className="mb-4 flex">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
{!hideTimeFormatToggle && (
|
||||
<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")}
|
||||
</label>
|
||||
<Switch
|
||||
name="24hClock"
|
||||
label={t("24_h")}
|
||||
defaultChecked={is24hClock}
|
||||
onCheckedChange={handle24hClockToggle}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user